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

Sorry for the confusion, @devoy, sometimes I remember to call them Desktops/Workspaces. Both Apple's doc and UI are pretty inconsistent. Mission Control calls them Desktop 1, Desktop 2, etc.

@ccstone / Chris, are you serious that AppleScript cannot see other Mission Control Desktops? That's absurd. And, unfortunately, I believe you. Do you know how to tell what is an AppleScriptable app and what is not? For instance, just this week I started using a new Markdown Editor called Typora. How could I look up somewhere or test if it is AppleScriptable?

I currently have 21 Desktops/Workspaces between two monitors. (I don't want to try imagining how to live without them.) I also currently have 23 TextEdit windows open. Using the Windows menu in TextEdit, I can get a list of the filenames, but it doesn't show me which Desktop/Workspace each one is in. That's what I was hoping I could entice KBM to do for me.

I guess I'll just have to step through each of 21 desktops and generate a list of what window are open and then filter, sort, and format the combined list into something useful. Definitely not a sweet solution.

But that just begs the question, How to List All Open Windows on a Desktop

Thanks.

Hey August,

I'm quite serious, but I did forget to mention one thing.

System Events can see the windows in the desktop you're currently IN.

It cannot see the windows that are in open desktops your are NOT currently IN.

This is just flaming stupid. Apple should have added a desktop property to System Events' window properties long ago, but they haven't.

For that matter Apple's UI for managing desktops continues to be abysmal – far better has been done with Mac utilities and on other flavors of Unix.

Of course – drop the app on the Apple Script Editor.app or on Script Debugger. If you see an AppleScript dictionary open the app is scriptable. HOWEVER – a “scriptable” app may or may not have more than the default AppleScript suite.

The default suite only adds a few commands and classes to work with – for example window bounds and window size and position.

Once you know what you're looking at you can quickly determine if an app has more than the default suite, but it takes a little education to get there.

Typora 0.11.5 is a perfect example of default-suite-only.

It hasn't seen any love from the author for years until this August.

The AppleScript default-suite is new, so maybe he'll add some Typora-specific commands – especially if users ask for them (hint).

-Chris

Thanks Chris.

Because he responded to a support question I had, I have the direct email address for Abner at Typora, so I requested better programmatic access, either an API or more AppleScript access than the default.

Hey August,

Great!

Would you point out to him that the default suite isn't fully connected to the app?

tell application "Typora"
   its class
   its frontmost
   its name
   # its properties -- fails
   its version
end tell

tell application "Typora"
   tell front window
      its bounds
      its class
      its closeable
      its document
      its floating
      # its miniaturizable -- fails
      # its miniaturized -- fails
      its modal
      its name
      # its properties -- fails
      its resizable
      its titled
      its visible
      its zoomable
      its zoomed
   end tell
end tell

tell application "Typora"
   tell front document
      its class
      its modified
      its name
      # its path -- fails
      # its properties -- fails
   end tell
end tell

Welcome additions:

  • Get/Set entire text of the given document.
  • Get/Set the selection of the front document.

That's all I can think of right off the top. Please add any specific requests you might have to my list.

-Chris

Thanks Chris,

I am a newbie at AppleScript. Is there a property that will tell you which desktop/workspace Any particular window is in?

If you’re willing to use JXA, the Core Graphics framework might be able to enumerate the full list of windows.

It contains some low-level C APIs, one of which in particular seems pertinent:

CGWindowListCopyWindowInfo().

It takes two arguments, which you can probably just pass 0 for each:

ObjC.import('CoreGraphics');
ObjC.deepUnwrap($.CGWindowListCopyWindowInfo(0, 0));
2 Likes

Hey @CJK,

Oh? That looks quite spiffy.

Looks like every window on the system is enumerated.

I just verified that it sees windows in desktops other than the current one too!

Good one!

Now one has to learn how to extract data from the collection, because you don't really want to parse that mass of text...

-Chris

I got a reply from Abner at Typora.

Currently Typora does not provide more AppleScript APIs than default, but I guess get opened window / file should be possible.

like macos - Listing all windows of all applications - Stack Overflow
or getting list of open windows - AppleScript | Mac OS X - MacScripter
or macos - Listing all opened documents across all open visible apps in AppleScript - Ask Different

Sorry, I'm not an expert for AppleScript, but hope those links help.

I haven't had time to look into those links but I posted them for anyone else following this thread.

Abner doesn't even have the default AppleScript suite hooked up properly.

None of the links he gave you is able to work with apps in spaces other than the one you're in.

The best hope so far is the code @CJK provided.

-Chris

Sorry for not checking back sooner.

Here’s a function that returns more manageable data:

ObjC.import('CoreGraphics');

Ref.prototype.$ = function() {
	return ObjC.deepUnwrap(ObjC.castRefToObject(this));
}

Application.prototype.getWindowList = function() {
	let pids = Application('com.apple.systemevents')
	          .processes.whose({ 'bundleIdentifier':
			        this.id() }).unixId();

	return  $.CGWindowListCopyWindowInfo(
		    $.kCGWindowListExcludeDesktopElements,
		    $.kCGNullWindowID).$()
			 .filter(x => pids.indexOf(x.kCGWindowOwnerPID) + 1
			           && x.kCGWindowLayer     == 0
					   && x.kCGWindowStoreType == 1
					   && x.kCGWindowAlpha     == 1
			).map(x => [{ id     : x.kCGWindowNumber,
			              name   : x.kCGWindowName,
			              bounds : x.kCGWindowBounds  
					   }]);
}

To use, e.g. to get Terminal’s windows:

Application('Terminal').getWindowList();

Hey @CJK,

I'm happy to see this.

Unfortunately it doesn't work on my Mojave system.

It seems to be breaking down here:

kCGWindowListExcludeDesktopElements,
          $.kCGNullWindowID).$' is undefined)

-Chris

Thanks Chris,

Since I'm on Mojave too, that's an important glitch to me.

1 Like

Hi Chris,

It appears to be a bit fixed in the current version of Typora, v1.1.5, out of Beta where it had been for years, and now costing $15 instead of free. (There is a free trial mode with a reminder/nag window that counts down the trial period.)

When I open Script Editor and look at the Typora Dictionary, all the items you listed above as "fails" are now defined in the dictionary doc. Does that mean they work now?

However, "selection" is not defined. Should it be? Or is it accessed another way? I don't find "selection" in the TextEdit dictionary either, so I think I must be missing a concept of operation here. When I google "applescript get selection" I get some advice on how to save the clipboard, send the ⌘+C keystroke, save that in a variable, and restore the clipboard. So it seems get selection isn't well supported in other scriptable apps.

If I create an AppleScript script something like this:

tell application "Typora"
    its visible
end tell

Is running that in the Script Editor enough? Do I need to make a KM macro to run it while Typora is frontmost? Putting it into the script editor with a delay 3 ahead of it lets me click the Run button and then make Typora frontmost.

When i use visible there I get an error and when I use frontmost I get true in the Results pane of the Script Editor. So I'm still not sure how to tell if the AppleScript support got fixed in the Typora 1.1 version. Can you tell?

Thanks for your help getting me this far.

Hey @ccstone / Chris,

I kind of got sidetracked into trying to make Typora work the way I would like it to, but that's not necessarily the direct path to my OP problem.

Were you ever able to get @CJK's function to work?

I'm realizing that one of the purposes I had for the original question (but not all) was to be able to tell one desktop from another, to tell when a desktop had been moved, etc. and I think I can do that using some one specific app that is well behaved that way. Maybe TextEdit, maybe something else. The idea here is to emulate what CurrentKey does invisibly by having some app that has one window on each and every desktop and using that to force window changes.

Maybe I build a small app, maybe I dedicate some app that I don't really use for anything else, so that leaves out TextEdit. Still sorting this one out.

About Mojave vs Catalina

I'm getting set to give up Mojave for Catalina. I don't use my 32-bit apps much anymore and there are free or affordable alternatives to the ones I do. Mostly the threshold for me is that sharing Notes between my iPhone and my Mac, on Mojave I can share only individual files with another person while on the iPhone I can share whole folders with someone else, and then they disappear from the Mac because Mojave doesn't grok shared folders. Moving to Catalina should fix this as well as give me updates to some other software that no longer supports Mojave.

I spent a day generating a list of all the 32-bit apps I had (there were 145 of them) and going through to see what I would miss if I deleted them (mostly Office 11 and a.bunch of Adobe stuff that I don't use or the licence has expired). The big one has been Acrobat Pro X, but some of the features I have been counting on are now in Acrobat Reader and others are available as free web utilites. Worst case is I subscribe for a month if there's something I HAVE TO have.

Sorry for the long Mojave/Catalina digression.

Did you test it for yourself ? I don't have Mojave, so I'm afraid I cannot, but I'm happy to report that it still works in Monterey. However, using the info that @ccstone provided about the possible error-prone statements, I've refactored the previous function to remove references to those elements:

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

As before, to use the function:

Application('<app name_or_id>').getWindowList()
2 Likes

Thanks @CJK, I appreciate your effort and I look forward to trying it. Unfortunately that could be a month away. Only being able to work on this every few months is frustrating. By the time I get back up to speed on the problem, my time window is nearly gone.

Could I ask a favor IFF you have time and interest? Would you be willing to wrap that function in a KM call to AppleScript (or whatever it takes) to demo it? Please don’t if it wouldn’t be fun.

I’m asking because I can get further faster if I can start with a working demo than if I first have to make something that works at all and then apply it to my problem.

Thanks!

On Big Sur at least, @CJK's very helpful (application prototype extension) code lets us generate a JSON listing of windows for named applications in this kind of Keyboard Maestro pattern:

Windows listed for named applications.kmmacros (4.0 KB)


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

    // Rough sketch by Rob Trew,
    // using CJK's Application prototype extension code.

    // Ver 0.2 (fixed a Width ⇄ Height swap)

    const main = () => {
        const
            appNames = Application("Keyboard Maestro Engine")
            .getvariable("appNames");

        return (
            extendPrototype(),
            JSON.stringify(
                lines(appNames).map(
                    k => ({
                        appName: k,
                        windowList: Application(
                            k.trim()
                        ).getWindowList()
                    })
                ), null, 2
            )
        );
    };

    // ----------------------- JXA -----------------------

    // Using code by CJK

    const extendPrototype = () =>
        Application.prototype.getWindowList = function() {
            ObjC.import("CoreGraphics");

            const
                pids = Application("com.apple.systemevents")
                .processes
                .whose({"bundleIdentifier": this.id()})
                .unixId();

            return ObjC.deepUnwrap(
                ObjC.castRefToObject(
                    $.CGWindowListCopyWindowInfo(16, 0)
                )
            )
            .filter(
                e => pids.includes(e.kCGWindowOwnerPID) &&
                e.kCGWindowLayer === 0 &&
                e.kCGWindowAlpha === 1 &&
                e.kCGWindowStoreType === 1

            )
            .map(x => ({id: x.kCGWindowNumber,
                name: x.kCGWindowName,
                bounds: [
                    "X", "Y",
                    "Width",
                    "Height"
                ].map(z => x.kCGWindowBounds[z])})
            );
        };


    // --------------------- GENERIC ---------------------

    // lines :: String -> [String]
    const lines = s =>
    // A list of strings derived from a single string
    // which is delimited by \n or by \r\n or \r.
        Boolean(s.length) ? (
            s.split(/\r\n|\n|\r/u)
        ) : [];


    // MAIN ---
    return main();
})();


2 Likes

Thanks CP/Rob (what do you prefer to be called, @ComplexPoint ?),

This looks like a great start, not only for my project but as a trailhead into JXA. I’ve started learning AppleScript to use with KBM and I have some familiarity with shell scripts, I’ve used SED with KBM to massage text lists, but I’ve had no inkling that JXA was available within KBM. Thanks for your great Wiki articles on the subject. I’m in a similar position to where @JMichaelTX was:

I used Acrobat JavaScript 20 years ago to manipulate comments in PDF files, but I have no experience using it in browsers.

Again, thanks, I look forward to trying this out.

Nice! I like your formatting (indentation) much more than mine.

I've just noticed that, for whatever reason, I've weirdly ordered the ordinates of the bounds property as X, Y, Height, Width] instead of `[X, Y, Width, Height. I'll make the correction to my post.

1 Like

Thanks ! Switched above too, now (in the macro and the listing).

formatting

( It's just the ESLint and Prettier ESLint extensions in VSCode )