Switch to Previous Desktop Space

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:

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:

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.


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.


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.


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.

I have no idea. I'm copying syntax here.

That makes a lot of sense and I'll give it a try.

Duplicate Notifications, duplicate processes

I got some insight into this error by reverting to a script that works. When I opened the app from Finder, when I changed spaces I got TWO identical notifications. When I opened the Notifications list,
I saw that there were pairs of entries where one entry was labeled for the script and one was labeled for Script Editor.

So running the app from Finder kicks of Script Editor running it too, if it's open. Closing Script Editor made the problem go away.

I didn't know either, so I looked it up.

From Handler Reference:


( on | to ) handlerName ¬
   [ [ of | in ] directParamName ] ¬
   [ ASLabel userParamName ]... ¬
   [ given userLabel:userParamName [, userLabel:userParamName ]...]
      [ statement ]...
end [ handlerName ]

It looks to me like there are two words so that you can form English-like constructions more easily, depending on what it is that you are doing.

For example, to quit conveys a slightly different English-syntax meaning than on quit . Saying on quit seems to me to imply receiving a quit signal, where saying to quit is different, it might be interpreted more like saying "To quit, find the entry in the Force Quit menu" and that meaning doesn't fit well in the context of the code.

Similarly for the second line in the reference template above, where of and in are synonyms: use whichever sounds better or more meaningful to you in context, is what it seems to me.

Indeed -- and, not really knowing what I was looking at I interpreted to notify as sending an event to notify...

I do love AppleScript's readability and the way you can usually make sense of what's happening without knowing much AS. But when real scriptures get to work, newbs like me get a bit lost!

Sorry for being silent. Ive got a lot on my plate at the moment. I read the various mishaps you've experienced, @August—that zombie process was a weird one, but that seems to be one of those things that happens from time to time when a process is unexpectedly orphaned somehow. It's not specific to AppleScript by any means, but it's prudent not to make changes to applications while they are still running.

Thank you, @Nige_S for following this closely and guiding August through this. All of your input has been bang on, most notably your observation regarding variable scoping. Indeed, the variables defined inside a handler (including the on run handler) are locally scoped so the ones being referenced in the notify: handler were undeclared. This would have actually thrown an error, but because the execution of the notify: handler takes place within a different context (it's called by an entity that isn't part of the AppleScript), it's unable to report anything bacl to the AppleScript parent object. This includes any errors thrown, which you won't know about other than that the rest of the code would fail to execute. It also cannot yield a return value. Therefore, whatever code is put inside the notify: handler needs to be oriented towards performing tasks in silence, and it needs to be error-free code. Variables from outwith the handler to which access is required need to be declared in the glohal scope, so either as properties belonging to or globals declared at the level of the top-level script object (ie. outside any and all handlers or other script objects). The options for getting information sent in or out of the notify: handler are limited, so you're kind of restricted to trafficking data in globally scoped entities. Don't forget that, as well as declaring a global variable at the top-level, you generally need to put a similar line inside the handler from which access to the global is required, as it's still possible to have local variables that share identifiers with global variables of the same name.

1 Like

Thanks @CJK,

I appreciate that detailed explanation, particularly the emphasis on

That concept helped me make a fundamental design decision.

All those variables are needed if I want to do the non-notification AppleScript actions here instead of as separate calls AppleScript from KBM. That makes this code unnecessarily complicated and therefor error-prone. What it would take to set up defaults and error conditions inside this notify: handler is way too much.

The other route, without all the variables, is to simply have this code be a trigger and nothing else. It tells KBM that an event has happened and KBM takes it from there. Simple, modular, easier to maintain, and with serious workflow benefits.

I complained about the AS development workflow of having to save it as an app and then close the Script Editor so that SE would not also run when I ran the App. Many additional steps every time I altered anything at all in the code. OTOH, if all this code does is trigger KBM, then it can tootle along in the background and I can do all the other querying, processing, and notification steps in KBM, which means that if I change the data displayed in a notification banner, that's it, it will happen that way on the next Space Change, via KBM.

Actually switching back to the previous space

So taking this back all the way to the OP question (the Forum UI keeps asking me, "Has your question been answered?"), the trigger we have now will allow KBM to keep track of all Space Changes, so it can always know what the previous Desktop Space was, by number or name, or whatever. The second part of the question is using that identification to actually do the switch to the new Space. @_jims's Desktop Spaces macros can do that for 16 or fewer spaces.

For me and my purposes, I want to be able to access Desktop 17 through Desktop 21, and there's no hotkeys definable for those. KBM can't simply define a key when there isn't an underlying OS operation, and System Preferences > Keyboard > Shortcuts > Mission Control only has operations defined for the first 16 Spaces.

I've done lots of research on Stack Overflow and Ask Different, finding threads about Desktop Spaces going back to Exposé. It seems that anyone who gets into this issue agrees that Apple is not supporting it well, support has gotten worse not better, and if it had better support it would be a powerful tool. People want support for more than 16 desktops, they want to be able to name desktops, and they want names to follow the Desktop if you change the order in Mission Control.

All of those were addressed by the CurrentKey app, which works for me, personally, now, but which is no longer available. So I'm still wanting to be able to do those things without relying on CurrentKey.

So far, on Stack Overflow and Ask Different, the state of the art is variations of the method that CurrentKey uses: place an app window on every desktop and use changes of window focus to get the OS to change Desktop Spaces. CK has its own simple app that simply creates windows and hides the windows behind the toolbar. Other SO and AD systems use things like Stickies (but Stickies itself has the downside that on a reboot, all the Sticky windows are collected on Desktop 1).

There had also been some systems that actually did rename the desktops in Mission Control, using code injection, but SIP prevents that, and I'm not willing to turn off SIP just for that.

So my next design decision is whether the Space-identifying app is hidden (like CurrentKey) or is visible, like Stickies (where people often use 72 or 120pt type so that the name of the desktop is visible in Mission Control). Many of my desktops have status or to-do lists on them, so I've also been considering letting the To-Do list be the identifying app.