How to List All Windows of One App That Are Open in All Desktops

You're welcome to use whatever method you wish, but there are good reasons not to leap straight into using other tools as a workaround for something. For one, other forum users will come by this post one day and it'll be benefit most people not only to see how the problems we encountered were solved, but also that it was solved and allows them to utilise the solution without third-party tools that they might not have nor might not wish to install. Secondly, debugging a solution that uses a number of different technologies is harder. Lastly, having a solution that is self-contained from beginning-to-end will usually (but not always) be the most performant.

My original contribution when I came across this post was simply to highlight a C API that enumerates windows regardless of which desktop (space) they are on. I later expanded this into a JXA function as the API call returns an excess of information (most of which is useless to most people), and of a data type a lot of people wouldn't know what to do with.

It wasn't intended to solve the exact problem you put forward, but I also knew (or was very confident in my belief) that there was no other means (that doesn't require installation of third-party tools) of obtaining window data for windows that weren't on the currently-active desktop. As far as I know, this remains to be true, and if anything, more so today than it was as Apple continues to tighten its security protocols.

I knew there was something off last time I replied. I couldn't put my finger on it, or must have been half asleep, but the fact that you're getting a data dump at all was all we needed. When I started to explain about data types and casting, etc., I was sort of doing this on autopilot, partly because what I was writing holds true when thinking about the problem in the context of my own system. On Monterey, this:

ObjC.import('CoreGraphics');
$.CGWindowListCopyWindowInfo(16, 0);

returns an opaque [Object Ref] that subsequently needs to passed to ObjC.castRefToObject() in order to get the "data dump" that you were already getting without this. This is obviously what changed between Mojave/Catalina and later version of macOS.

Therefore, the solution is simply to remove the call to ObjC.castRefToObject() so that this line:

return  ObjC.deepUnwrap( ObjC.castRefToObject(
        $.CGWindowListCopyWindowInfo(16, 0) )).filter(e =>

is now simply this:

return  ObjC.deepUnwrap($.CGWindowListCopyWindowInfo(16, 0)).filter(e =>

The rest is just regular JavaScript, so can be left alone.

I'm such an idiot.

Exploring this further, it doesn't look like information about which desktop a window is on is returned by the API. To double check, here's a list of all possible keys that can appear for any individual window (𝑡he first part of the list are Required Window List Keys that will always be included, and the latter part of the list are Optional Window List Keys that may or may not be included):

$.kCGWindowNumber
$.kCGWindowStoreType
$.kCGWindowLayer
$.kCGWindowBounds
$.kCGWindowSharingState
$.kCGWindowAlpha
$.kCGWindowOwnerPID
$.kCGWindowMemoryUsage

// $.kCGWindowWorkspace — deprecated in macOS 10.8
$.kCGWindowOwnerName
$.kCGWindowName
$.kCGWindowIsOnscreen
$.kCGWindowBackingLocationVideoMemory

I wonder whether $.kCGWindowWorkspace used to hold the specific information about which desktop a given window was on, but it no longer works. However, what will provide a little more help for you is the key $.kCGWindowIsOnscreen, which does at least inform you whether a given window is on the currently active desktop or not.

Here it is implemented in the previous function declaration:

Application.prototype.getWindowList = function() {
        ObjC.import('CoreGraphics');
        let pids = Application('com.apple.systemevents').processes
                   .whose({'bundleIdentifier':this.id()}).unixId();
        return  ObjC.deepUnwrap($.CGWindowListCopyWindowInfo(16,0))
                .filter(e => pids.includes(e.kCGWindowOwnerPID)  &&
                                           e.kCGWindowLayer==0   &&
                                           e.kCGWindowAlpha==1   &&
                                          !e.kCGWindowIsOnscreen &&
                                           e.kCGWindowStoreType==1)
                .flatMap(x => [ { id     : x.kCGWindowNumber,
                                  name   : x.kCGWindowName,
                                  bounds : [ 'X', 'Y',
                                             'Width',
                                             'Height' ].map(z => 
                                           x.kCGWindowBounds[z]) } ]);
}

Application('Script Editor').getWindowList();

That will return the list of windows that are on desktops other than the currently active one. To return only those windows on the currently active desktop, remove the ! from the beginning of !e.kCGWindowIsOnscreen.

Um, for not getting it perfect the first time? I don’t think so.

Thanks for ALL your help. I’ll explore this new context.

The approach I was talking about was using a shell script and built-in tools, not third-party tools. I often find it easier to follow the UNIX philosophy of building a one step at a time pipeline instead of trying to do the entire reformatting, filtering, etc. process inside something like JavaScript.

Yes, the Apple doc says that’s exactly what it was for— and it only worked from 10.5 to 10.8. That’s probably what HyperSpace (which did a lot of what I’m trying to do) was dependent upon.

You're most welcome. I've been away from the KBM forum for a long while, so it's a nice way to get back into it.

That's not unusual. It's the nature of the imperative programming style, in which one operation is performed coded in a line, followed by a second operation coded on a second line, etc. JavaScript is perfectly capable of being written this way. The code you've seen above leverages JS's multi-faceted nature, in being a traditionally-imperative language, but having incorporated a lot of functional syntax and attributes that you're seeing a little of in those code snippets. I was trained in my formative years in functional languages before moving onto imperative ones (bar some toying around with BASIC and C back in school), and it's a lot easier transitioning in that direction. But the majority of professional developers (which I haven't been for a long while, hence I'm quite rusty) all started (and largely remained) with imperative languages, so the slight uphill climb to get one's head around a more functional approach one worth doing and one you won't be doing alone.

If you would prefer the JavaScript function to output a result as a string instead of a JavaScript array of objects, that is perfectly doable. It sounds like the JS object array, whilst containing most of the pertinent information you might need/want, might not be something you're comfortable handling and extracting specific bits of information out of...? Would something like a CSV-style output be more useful for you ?

Most of my personal scripting stuff nowadays is done on the command line. I don't know what it is about shell scripting, but I do find it very addictive. Most of my heavier coding is done in Swift now, but Swift lends itself quite reasonably as a scripting language as well, so I actually do most of this from the command line too.

What shell do you use ? zsh, presumably ?

When I run the following script from @ccstone

by putting it inside a KBM Execute Applescript action, I get a list delimited with the combination comma-space. I need to change that comma-space delimiter.

Unfortunately, I know I have filenames that have that comma-space character combination in them, so I can't break the resulting list at that character combination without risking breaking a filename sometimes, and also I'd have to remember to never, ever use that in filenames, which I can't count on either. It is, after all, both legal and useful.

How can I change the list delimiter in AppleScript to be something other than comma-space?

I did some digging and tried the following:

image

tell application "TextEdit"
	set AppleScript's text item delimiters to "|"
	set winNameList to name of every window
	set AppleScript's text item delimiters to "" -- ALWAYS SET THEM BACK
end tell
return winNameList

to attempt to set the delimiter to the pipe symbol (|) and it had no effect, the KBM window still showed comma-space between filenames.

Ideally, I'd use a newline, but I thought I'd do testing with a visible character first.

I then tried to see if I could change the delimiter as the list was built, using:

tell application "TextEdit"
	set winNameList to ""
	repeat with theItem in name of every window
		set winNameList to winNameList & "||" & theItem
	end repeat
end tell
return winNameList

But Script Editor complained, "Can’t make item 1 of name of every window of application "TextEdit" into type Unicode text." while highlighting the variable theItem at the end of the concatenation.

That tells me that it appears to be interpreting my repeat with theItem in name of every window properly, and the problem is in building the list. But what else is that item if it isn't Unicode text?

Any ideas?

TextEdit -- delimited list of window names.kmmacros (1.7 KB)


Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    return Application("TextEdit")
    .windows
    .name()
    .join("|");
})();

Or if that's literally all you're doing:

Expand disclosure triangle to view JS Source
Application("TextEdit")
.windows
.name()
.join("|");

If you prefer AppleScript, it is, as usual, a bit messier – requiring more work, but still possible:

on run
    tell application "TextEdit" to my intercalate("|", name of windows)
end run



-- intercalate :: String -> [String] -> String
on intercalate(delim, xs)
    set {dlm, my text item delimiters} to ¬
        {my text item delimiters, delim}
    set s to xs as text
    set my text item delimiters to dlm
    s
end intercalate

or

on run
    tell application "TextEdit" to my intercalate(linefeed, name of windows)
end run



-- intercalate :: String -> [String] -> String
on intercalate(delim, xs)
    set {dlm, my text item delimiters} to ¬
        {my text item delimiters, delim}
    set s to xs as text
    set my text item delimiters to dlm
    s
end intercalate
1 Like

Thanks @ComplexPoint, that JXA code is simple and sweet.

Could you elaborate on what the use strict and return constructions do in this case? That is not literally all that I am doing.

As I mentioned above, I'm using "|" for testing; ultimately I want a newline there, so that I can sort and filter the list and treat items one at a time. Will .join("\n"); do it?

Thanks also for the AppleScript versions. I have the idea that eventually this will be for public consumption, so I hesitate to mix JXA and AS in the same macro suite, but what's easy is easy. Extra motivation to learn more JXA too.

In the AS code, yes I'll be using linefeed version. Thank you!

The term "intercalate" is new to me. Googling, I found interesting examples from microbiology, geology, and biblical studies, but no coding definitions that I found lazily. Are you using it as simply an appropriate name for the operation, or is it a predefined term in AppleScript?

Thanks!

See:


an appropriate name for the operation

Yes, a user-defined function like that would work as well by any other name.

Wow. Thanks, that's a great exposition of what seems like a relatively complex subject. The language interpreter is operating at a level of abstraction that most scripters don't think about, and your explanation makes how you have to deal with that make sense.

It isn't a list delimiter you change -- it's AppleScript's text item delimiter. It's what text is split on when you convert it to a list and what list items are joined together with when you cast a list to text. (The ", " you mention is just Script Editor's and others' visual representation of the "list item divider".)

So:

tell application "TextEdit"
    set {oldTIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "|"}
    set winNameList to (name of every window) as text
    set AppleScript's text item delimiters to oldTIDs
end tell
return winNameList

..where you are setting the text item delimiter (and saving the old one for later), getting the list of window names with (name of every window), casting from list to text -- using your set delimiter as the separator -- and saving in winNameList, and resetting the text item delimiters to their original value like a polite AppleScripter should.

That is clearly the step I was not conceptualizing clearly.

I suspect I was thinking in terms of KBM, where everything is text.

Thanks!

1 Like

There's really no need to do this. The important thing is to always make sure you actively set the text item delimiters before any instance of list to string conversion. This should1 be something that AppleScripters do already, but not many often remember, despite it beiing the only step necessary to prevent unexpected results. Yet these same scripters cling with their lives to this obsession over restoring the delimiters back to whatever, which seems to require 5 lines of code in a lot of versions of that text replacement handler that I see as the AppleScript equivalent of my mother unplugging the microwave and toaster every time before leaving the house, and still having to check wirh my dad that she unplugged these things when they arrive at their destination. That's how pointless resetting the delimiters is.


1So, even this isn't an absolute necessity. This is all from some 20-year-old version of AppleScript that once instantiated a single process within which it executed all AppleScripts. The environment, therefore, would carry the settings left over from the previous run of one script and be in effect during the run of another script. This included the values assigned to AppleScript's text item delimiters, which someone problem-solved this (albeit in the wrong direction) by establishing a "good practice" habit of resetting the delimiters at the end of all scripts. Reasonable logic, and it caught on, but the optimal solution would have always assumed the delimiters to be some unknown value (which would cater for situations where a background script was run without being overt and didnt reset the delimiters, or one's own oversight causes the delimiters to persist through). Whereas, if one's only focus is in setting them prior to any and all list to string conversions, then it's the only measure that is entirely under one's own control, and the one making the fewest assumptions about the AppleScript environment, plus it's an action occurring only when necessary rather than as a neurosis.

Of course, none of this applies any more except in one or two very rare scenarios: Script Editor, Autimator, Script Debugger, and FastScripts all instantiate individual AppleScript processes every time a script is recompiled and executed. So you always start off with a fresh set of delimiters. KM last i checked executes its AppleScripts using osascript, which terminates at the end of its job run. It's possible nor to have an osascript instance destroyed, and there's one application I forget the name of that persists a single instance and recreates that 20yo situation. In fact, it might been FastScripts before the dev rejigged the entire implementation to what it is now.

1 Like

Totally agree -- and thanks for explaining it better than I ever could!

If I'm writing for myself I usually don't bother resetting. But if I'm writing a snippet for someone else I can't be sure how they are going to use it, so it's a "safety first" measure.

So if someone writes:

set AppleScript's text item delimiters to "/"
repeat with eachItem in pathList
    set foldersList to every text item of eachItem
    -- do stuff
end repeat

....because "I've set TIDs then used them", and then wants to do further text processing in do stuff, I don't kill their script by changing their assumed TIDs.

Yes, it would far more correct if their original script was:

repeat with eachItem in pathList
    set AppleScript's text item delimiters to "/"
    set foldersList to every text item of eachItem
    -- do stuff
end repeat

...but since I can't guarantee that I use the reset safety belt.

Of course, I may just be excusing a 20-year habit that I really should shake :wink:

1 Like

Oh no no no, that would be awful as the delimiters are being set to their present value in every iteration of the loop. So unless do stuff... includes code that changes the delimiters as well, then you were right the first time:

If I'm handing this off to someone who intends to insert this into their script, I will quickly educate them about text item delimiters, and hopefully instill in them from the beginning the best practice. As I would normally be walking them through the script anyway to explain what each part does (if they are novice, that is), then it actually forms an inherent part of that explanation. If they're not novice, but not seasoned either, then i guess i wouldn't give an exposition on what the script is doing, but might say "Note that this changes the AppleScript delimiters, as they're involved in splitting the filepath string into a list of its components. So just make sure that if you end up joining this list (or any other list) up later on in the script, always make sure you set the delimiters to an appropriate value right before you do so."

But yeah...it gets challenging sometimes figuring out how to say these things without sounding like a condescending twat. Sometimes i manage it, and sometimes i don't. So I don't blame you for finding a way out of having to school someone if all it takes is to bung in that ubiquitous text replacement handler at the bottom of a script or something similar.

Except, as was pointed out to me last year by one of my users, it is a very literal interpretation of my advice to them to "always set your TIDs before using them".

And yes, this all came about because they asked me for a path-splitter. A month later they asked me for a way to split text on spaces and I, stupidly, said "It's the same as the one we did before except you change the / to a space", not considering where they would be using it. And they wrote something like:

set AppleScript's text item delimiters to "/"
repeat with eachItem in pathList
    set foldersList to every text item of eachItem
    -- do stuff, including
    set AppleScript's text item delimiters to " "
    repeat with eachItem in foldersList
        set textList to every text item of eachItem
        -- more stuff
    end repeat
end repeat

Again, totally reasonable from their point of view as it was the same as we did before! A little too the same...

In the "before times" this wasn't such a problem as we'd usually be sitting down together and going through things, just as you describe. But we've still got a lot of people working from home -- and in this case, "home" was 10 time zones away...

So I'm afraid I've gone back to resetting TIDs, just in case, when doing snippets for others. While it's often a pair of pointless operations (which I dislike on principle) it's only a line and a half of extra code. And for me it's the same amount of typing -- TID-swapping is two of the few KM text expansions I've written!

Yeah, true, he ain't wrong.

Ach, well, if it's not going to destroy the world, then there's efficiency in simply doing what we're most comfortable with.

1 Like

Getting back to the core of the OP:

I ran a series of tests with the Finder folder DeskSpaceIDs open in the next Desktop Workspace, Finder lists it in its Windows menu.

However, running this from KBM does not work:
image
(Does not work)

KBM cannot "see" the window in the Desktop, possibly because System Events cannot see other Desktops. For example, after verifying in the Finder Windows menu that the folder is still open in a window, this script does not work:
image
(Does not work)

However, Finder knows about its own windows and can act on them in response to AppleScript. This version does work:
image
(Does work)

Moving the ball forward ...