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

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 )

I got a chance to download an install "Windows listed for named applications.kmmacros" and what I get when I run it is:
image

I'm running on Mojave (on the verge of upgrading to Catalina). Would that make a difference?

How do I capture the rest of that error message? In a shell, I could redirect STDERR to a file. Should I have the shell execute the JavaScript to capture that, or is there a KBM-native way?

Ah HA! I found the logs folder /Users/amohr/Library/Logs/Keyboard Maestro/ and the Engine.log file.

Here's the result of running the macro:

2022-08-07 18:00:44 Execute macro “Windows listed for named applications” from trigger Editor
2022-08-07 18:00:45 Action 11176861 failed: Execute a JavaScript For Automation failed with script error: text-script:1137:1174: execution error: Error on line 44: Error: Ref has no type (-2700)
2022-08-07 18:00:45 Execute a JavaScript For Automation failed with script error: text-script:1137:1174: execution error: Error on line 44: Error: Ref has no type (-2700). Macro “Windows listed for named applications” cancelled (while executing Execute JavaScript For Automation).

It identifies line 44.

Here's lines 43-47 of the function text:

            return ObjC.deepUnwrap(
                ObjC.castRefToObject(
                    $.CGWindowListCopyWindowInfo(16, 0)
                )
            )

I'm not sure what "Ref has no type (-2700)" means, but I'll try running the script without the "strict" setting.

No difference. It's probably not just a JavaScript error then, I guess it's coming from ObjC. Stack Overflow has nothing for that error text.

Any suggestions?

It looks like it's taking issue with whatever gets returned by the function $.CGWindowListCopyWindowInfo(16, 0). Thus I'd want to know what value it's returning.

What gets returned by the following code:

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

// ObjC.castRefToObject(wInfo);

Subsequently, if you remove the two slashes at the beginning of the last line (this uncomments the line, which at present doesn't get executed), what does the code return now ?

[ I'd suggest running this in Script Editor: you can create a blank document and set the language to JavaScript at the top of the document (there's a dropdown menu). Also, from the bottom of the document, in the status bar, there are three icons on the left. Click the third one to reveal the event log. The reason to use Script Editor here is that we're dealing with rather opaque values that KM won't be able to display natively. The ObjC.castRefToObject function is integral to converting an opaque value into one that, firstly, JXA-ObjC can understand, before this gets unwrapped into something JS can understand, before it's manipulated to produce an array of dictionaries that KM should be able to understand. ]

1 Like

As a first try, I opened Script Editor, opened a new File window, set it to JavaScript, and pasted in the lines above. I opened the Events log and ran it (with the Play button). That produced 113038 characters of output in a single "line". The line starts out with "$([" and the matching "])" is at the end, 113035 characters later.

So I tried uncommenting that fourth (fifth) line above, When I did I got:
image

And in the Events pane I got:

Error -2700: Script error.

That looks familiar.

So, any idea what's going on? Why did you ask for that line? It's not in the original function, is it?

Back at the test script with the line commented out, all 113038 characters of it --
Within that overall ([...]), it looks like there are repeated blocks of

$({"kCGWindowLayer": ... }),

So for readability, I'll add linebreaks after all those commas, using a VIM global.

:g/}),$({"kCGWindowLayer":/s//}),^M$({"kCGWindowLayer":/g

That results in 337 lines, one per kCGWindowLayer.

Any idea what I should be looking for in there?

Aside:
97 of those lines include

"kCGWindowOwnerName":$("Logi Options Daemon")

That's for my bluetooth mouse, which isn't even on right now. There is only one entry for that in the Activity Monitor (All Processes), so why so many entries here?

Thanks!

I’m curious where you got those parameters, (16, 0).

The Apple doc for CGWindowListCopyWindowInfo describes them as:

static var optionAll: CGWindowListOption

var kCGNullWindowID: CGWindowID { get }

Where did you get numeric values?