Switch to Previous Desktop Space

Getting back to the OP question about switching to the previous desktop space.

@_jims's macros are very useful and seem to be the current state of the art for KM Desktop switching. But as @_jims will tell you, you have to switch windows using his macros for the system to keep track of where you are so that "previous" in the system matches what you think it ought to be.

Unfortunately, there are many ways to change Desktop Spaces, intentionally and unintentionally, without Jim's system, and in those cases, his Previous macro or menu choice won't be what you want it to be. For a simple example, you could be in Acrobat Reader and click on a link inside the PDF. Your default browser will open up a new tab in the most recently used browser window, which may or may not be in that same Desktop Space, and without jumping through hoops before you click on that link, you cannot tell. Suddenly finding yourself in another Desktop Space, of course you will want to return to where you were, and Jim's Previous macro will not be of any help, and if you don't happen to remember, then you have to hunt for it.

That's why I asked the question in the first place. seven months ago, and why, even though Jim's macros are very useful, I haven't market this issue as Solved.

I've come to the conclusion that any working system will require two pieces of information:

  • The ID of the current desktop, recorded somewhere to be the "previous" location after a move, recorded at every move.
  • A "trigger" to notify you when anything, user, browser, Finder, Terminal process, or whatever, changes the current Desktop Space.

There's good progress on the first of those above, from @_jims, and over in the thread How to test for a Space (Mission Control)?.

Ideal for the second would be a Keyboard Maestro trigger that the Desktop Space has changed, as I asked about in the OP. We don't have that (yet), but it sure would be nice, wouldn't it?

What if there was a way to create a "virtual" trigger? I think I have one, but I'm not the programmer (yet) to implement it.

I was recently given an "Amulet of Revealing" by the developer of the CurrentKey app. He said:

My swift code listens to this specific MacOS api, here is the line:

NSWorkspace.shared.notificationCenter.addObserver(forName: 
    NSWorkspace.activeSpaceDidChangeNotification, 
        object: nil, queue: nil, using: 
            <the function you want to handle this event> )

I'm not a Swift programmer. That's the first line of Swift code I've actually had an interest in using. I don't even know if I added linebreaks to the above in the correct syntax. What I did would not work for AppleScript.

So I need some help from the Swift programmers on this forum. I'm tagging @CJK, @ccstone, @DanThomas, @ComplexPoint, @MitchellModel, @peternlewis, and @_jims, as folks who I remember participating on this topic in various threads. My apologies to anyone I forgot and to anyone I tagged who isn't interested.

Please help me correct my misunderstandings:

The first part ends in .addObserver so that says to me that this line only needs to be run once and an Observer will be created that hangs around. Is that right?

Does the Observer terminate when the current code stops running, so the code has to stay open, or what? Is that Observer permanent until reboot? I really don't know how this works.

When the Observer is running, it's watching for this notification called NSWorkspace.activeSpaceDidChangeNotification to turn true. When that happens, because of what we're doing here there's no "object" involved (whatever that would be) and no "queue" involved (again, whatever that is), but it will use the function name that I provide to it, "to handle this event".

Can that function be an AppleScript function, or an AppleScript function inside a Swift wrapper, so that the AppleScript function can call a KM function?

If that Observer can indirectly call a KM macro as the "handler", then it seems that's getting most of the way there to having a virtual KM trigger of the Desktop Space being changed.

Am I right? What am I not understanding? What needs to be done to make this work so that KM can start this Observer, if it's not already running, and then get a macro triggered every time anything changes the current Desktop Space?

Thanks!

1 Like

Yes.

The code would have to run and continue running and monitor that and trigger a macro when it happened.

It could probably be done in JavaScript for Automation. If not, it would require a small application.

I will add it down as a potential trigger for a future version. I'm glad to see there is at least some Spaces API that is public.

3 Likes

Thanks Peter.

So I would need a small utility (perhaps run from a Terminal window) called something like WatchDesktop or KMDSObserver that would start this Observer and just stay there. From a terminal window, it could be run in the background. Would that be enough?

Are you suggesting that the function that it calls should be in JXA? Or that JXA be used to run this Swift code? Or that this API ought to be available in JXA if it's in Swift?

Since it's a high priority for me, and I know you don't like to make promises of what functions will be in the next update until you've gotten them working, I'll continue banging on this. If you get it working next week, please let me know!

Hi @August. Actually with almost all situations, including the one you describe, the macros I provided will properly track the Current and Previous Desktop Space. This is done using the macro Go to Previous Desktop with the trigger: The focused window: The focused window changes

Notice that I said almost all situations. I recommend using the macros Go Left a Desktop, Go Right a Desktop, etc. when doing explicit space changes because this will guarantee that the Current and Previous Desktop Spaces are properly tracked within the logic of the macro set.

From a practical standpoint, I've found that the rare situations that invisibly change the Desktop Space (from the standpoint of the macro set) cause minimal inconvenience. (I use these macros hundreds of times during the typical week.)


With the above said, my macros could be greatly simplified if @peternlewis was able to add a Space Change trigger to Keyboard Maestro.


If you are looking to roll your own space change detector, I suggest you consider digging into the WhichSpace code. Obviously the developer, George Christou, has determined a method.

1 Like

Thanks for the clarification, Jim (@_jims),

Sorry I misrepresented your macros. That was my recollection of the difficulties that I initially encountered. Maybe I exaggerated in my memory or maybe I wasn't patient enough to learn new habits that worked with those macros. However it happened, I'm very glad to know that they aren't as problematic as I had been painting them to myself. Sorry for the complaints.

However, if simple Go Left and Go Right worked for me, I wouldn't be chasing this. If I had three, five, or nine Desktops, that would probably suffice. And yes, sometimes I can rearrange the order of the Desktops so that the Spaces I am using most are next to each other. That's often the case when I'm doing any kind of programming where I have my working edits in the middle, doc in the Desktop to the left of that and testing in the Desktop to the right. I use Go Left and Go Right a LOT when I'm working on something like that. But for me that's a limited case with 3 desktops out of 20. I have several projects that are in ideation, research, and collecting mode. That's one group of four Desktop Spaces, currently. I have three Desktops that are for ToDo lists, status tracking, and filing/archiving, plust a couple more because I'm testing out different apps in parallel on different projects. I have two for email, one for FaceBook, and sometimes those aren't enough. So usually I'm jumping from one group to another group, and having to know that there's five steps in between is an annoyance, not a help.

Thanks,
August

Hi @August.

I use 16 Desktops and most of the space changes (and window movements) are to non-adjacent Desktop Spaces.


My macro/subroutine set can be used two main ways:

1. As is with or without a Conflict Palette.

Here's the CP that summarizes the capabilities provided:

Conflict Palette-image

In the palette you'll see that there are actually 40 different actions that can be completed using the stock features of the macros (40 = 2*16 + 8).

I sometimes use the conflict palette, but it's mostly designed for beginners or occasional users. I typically use the configurable hot keys assigned to the 40 actions... or, more often, the second method.

2. Creating and using additional simple macros that leverage the subroutines provided in the macro/sub-routine set.

This enables a context sensitive use of desktop spaces.

The best way to understand this method would be to read a post I submitted elsewhere: Workspaces in Mission Control Desktop Spaces

But this Group Palette might give you a general idea:

Group Palette-image


@August, elsewhere you have mentioned that you use more than 16 Desktop Spaces. The macro/subroutine set is limited to 16 because macOS does not provide shortcut keys for more than 16.

With said, if you'd like to try applying these macros and subroutines to your situation (e.g., let's say you have 25), I see at least one possibility...

Applications that are not necessary assigned to a space could be used in one or more of the first 16. (When I use these desktop spaces I do not assign applications to specific space even though the default use of many applications is normally on one of them. (See the Group Palette image above). In contrast, there are two applications that I've chosen to appear on all Desktop Spaces: Keyboard Maestro and Tot. When I don't want to see them, I simply use Hide (⌘H).)

Desktop Spaces 17 to 25 could then be used for applications that you use in an isolated fashion and would be assigned to one of these eight using the normal macOS setting (i.e., Right-click the application in the dock>Options>Assign To: This Desktop). You could then switch to these spaces by opening (or activating) any of these eight applications when needed. Once using one of these spaces, you could return to any of the first 16 using the macros in the set.

@August, if are interested in working within these constraints, let me know. I would need to modify and test one subroutine: sub—Put DesktopNo and/or DesktopNo_prev

My apologies for the slow response. I didn't see the notification that you had tagged me.

Yup.

It'll exist for as long as the application running the code continues to run.

For someone who doesn't know, you're a very good guesser.

A great deal of the AppKit framework is included in the AppleScript-ObjC bridge, which includes the NSWorkspace class and its methods; and the NSNotificationCenter class is part of the Foundation framework. So, yes, the function can be an AppleScript handler. However, not through the specific implementation you've exemplified in the Swift snippet.

There are two NSNotificationCenter class methods for adding observers for notifications. In Swift parlance, these are addObserver(forName:object:queue:using:) (which is the function you were shown); and addObserver(_:selector:name:object:). Of course, Swift APIs can't be called from AppleScript, while the ObjC methods are at our disposal, respectively addObserverForName:object:queue:usingBlock: and addObserver:selector:name:object:. Well, one of them is. Functionally, they're both quite similar, but the first method (which is the equivalent to function used in your Swift snippet) requires your custom function to be passed as a block (like an in-line function for which there is no equivalent AppleScript construct). So instead, you'd create your observer using addObserver:selector:name:object:, where the selector can be an AppleScript handler.

Not quite. KM won't be acting as a trigger, not even indirectly. Remember, the code that instantiates the observer has to continue running for the observer to remain active, and I don't recommend keeping a KM macro running on an infinite loop. Rather, you'd create an AppleScript Stay Open application that stays idling in the background and receives notifications that it's registered to listen for. It contains the handler that will be called whenever a notification is observed, and there's no reason that this handler couldn't then script KM or the KM Engine. But it's this handler that will be your trigger, not KM.

I'll post an example AppleScript implementation.

1 Like

Honestly, I'm a little surprised it worked, let alone as well as it did. It detected every space change. Sadly, it's not forthcoming with information, such as which space you've changed into or come from.

Here's the barebones script:

use framework "AppKit"
use scripting additions

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

on run
		tell my NSWorkspace's sharedWorkspace's notificationCenter ¬
				to addObserver:me selector:("notify:") ¬
				|name|:(my NSWorkspaceActiveSpaceDidChangeNotification) ¬
				object:(missing value)
		
		close access (open for access msgs)
		set eof of msgs to 0
end run

on idle
		
end idle

on quit
		tell my NSWorkspace's sharedWorkspace's ¬
				notificationCenter to removeObserver:me
		
		continue quit
end quit

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:

This is a Stay Open application, which is one of the options available when choosing which format to save a script as in Script Editor. On running the application, it registers itself as an observer. Then it idles, and does absolutely nothing until you change space. It receives the alert, and the notify: handler is called (there's nothing useful passed to it in the message parameter, but it has to be there; you can name it whatever you want though). All I had it do was log the timestamp at which the space change occurred, and throw up a macOS notification that popped up in the top-right of the screen during changes. As proof:

Have to admit, that was pretty nifty.

NSNotificationObserver.app.zip (55.4 KB)

2 Likes

Me too, very slick indeed! Thanks for sharing, @CJK

I noticed that the notifications you indicated above appear if I run your AppleScript from the Script Editor, but not if I put it in a Execute an AppleScript. Any thoughts on that?

I haven't dug into the code, but as I shared above with @August, developer George Christou has a method to determine and display the Desktop Space number in his WhichSpace app.

Thanks @CJK !

Sorry for the mangled English syntax. I meant that the Observer would call AppleScript which would call a KM macro, so the KM macro would essentially, indirectly, "handle" the Observer's notification. That would make the whole thing a virtual KM trigger, like other KM triggers, in the sense that the specified KM macro would run when the event occurred.

Yes, that's what I was trying to say.

It would, of course, be limited to a single KM macro, but that one macro could execute as many KM macros as you want. That's a bit more convoluted than the way KM allows the macro definition to specify its own triggers, but it would work until Peter is able to find time among all his other priorities to implement and test this.

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.