Get an ordered list of all open windows

While I was coming up with a hacky-but-functional way of swapping the frontmost windows on two displays, I was wondering if there were a better way to do it.

I didn't think there were any Keyboard Maestro tokens or functions that provided such a list, and I don't believe you can get window order information directly from AppleScript. So I asked Claude, and it came up with a piece of Swift code that worked great. I had it return the app name, window name, and frame, and I thought I was set.

But it turns out that the Cocoa method returns the title bar text as the window title, and that's not always the case. Mail, for instance, has two lines of text in the title bar—the name of the mailbox and the message count. The script was only returning the first line, which meant that window wouldn't be found if I tried to activate it.

The answer to that problem is actually AppleScript, because you can easily get the names of an application's windows. But as it seemed ungainly to run two scripts for one piece of data, I had Claude integrate the AppleScript into the Swift script. Here's the final script:

Apr 8 Version 3: Now offers JSON or TEXT output, and the script has been further optimized for speed. Removed accidental links to my timer macro I left in place (whoops).

Apr 8 Version 2: AppleScript is out, AXUIElement... is in. I fed Claude's script to ChatGPT, and asked it to optimize it. ChatGPT said AppleScript is slow, this way is faster…and it's much faster; it takes about a second now on my Mac with a lot of windows open, when it was three or so seconds before. And you can make it quicker, too, if you compile the script, and run the compiled version—instructions for doing that are in the macro.

Here's the code that's in the macro, in case you'd like to look it over before downloading—this is the JSON version:

The following code is 100% AI-generated. Use at your own risk.

import Cocoa
import ApplicationServices

struct WindowInfo: Codable {
    let app: String
    let window: String
    let x: Int
    let y: Int
    let width: Int
    let height: Int
}
struct WindowData: Codable {
    let count: Int
    let windows: [WindowInfo]
}

func getAXWindowTitlesByFrame(pid: pid_t) -> [CGRect: String] {
    let app = AXUIElementCreateApplication(pid)
    var value: CFTypeRef?
    guard AXUIElementCopyAttributeValue(app, kAXWindowsAttribute as CFString, &value) == .success,
          let windows = value as? [AXUIElement] else { return [:] }
    var result: [CGRect: String] = [:]
    for window in windows {
        var minimized: CFTypeRef?
        AXUIElementCopyAttributeValue(window, kAXMinimizedAttribute as CFString, &minimized)
        if (minimized as? Bool) == true { continue }
        var posVal: CFTypeRef?
        var sizeVal: CFTypeRef?
        var titleVal: CFTypeRef?
        AXUIElementCopyAttributeValue(window, kAXPositionAttribute as CFString, &posVal)
        AXUIElementCopyAttributeValue(window, kAXSizeAttribute as CFString, &sizeVal)
        AXUIElementCopyAttributeValue(window, kAXTitleAttribute as CFString, &titleVal)
        guard let title = titleVal as? String, !title.isEmpty else { continue }
        var pos = CGPoint.zero
        var size = CGSize.zero
        if let p = posVal { AXValueGetValue(p as! AXValue, .cgPoint, &pos) }
        if let s = sizeVal { AXValueGetValue(s as! AXValue, .cgSize, &size) }
        result[CGRect(origin: pos, size: size)] = title
    }
    return result
}

guard let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID) as? [[String: Any]] else {
    print("No windows found")
    exit(0)
}

var axCache: [pid_t: [CGRect: String]] = [:]
var results: [WindowInfo] = []

for w in windows {
    guard
        (w["kCGWindowLayer"] as? Int) == 0,
        let pid = w["kCGWindowOwnerPID"] as? pid_t,
        let appName = w["kCGWindowOwnerName"] as? String,
        let bounds = w["kCGWindowBounds"] as? [String: CGFloat],
        let x = bounds["X"], let y = bounds["Y"],
        let ww = bounds["Width"], let h = bounds["Height"]
    else { continue }

    if axCache[pid] == nil {
        axCache[pid] = getAXWindowTitlesByFrame(pid: pid)
    }

    let frame = CGRect(x: x, y: y, width: ww, height: h)
    let cgName = w["kCGWindowName"] as? String ?? ""
    let axName = axCache[pid]?[frame] ?? ""
    let windowName = axName.count > cgName.count ? axName : cgName

    guard !windowName.isEmpty else { continue }
    results.append(WindowInfo(
        app: appName,
        window: windowName,
        x: Int(x), y: Int(y),
        width: Int(ww), height: Int(h)
    ))
}

let output = WindowData(count: results.count, windows: results)
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
if let jsonData = try? encoder.encode(output),
   let jsonString = String(data: jsonData, encoding: .utf8) {
    print(jsonString)
}

And here's a simple demo macro that creates a list of apps, windows, and frames, with the most recently used window at the top:

Download Macro(s): Get all open windows.kmmacros (21 KB)

Macro screenshot

Macro notes
  • Macros are always disabled when imported into the Keyboard Maestro Editor.
    • The user must ensure the macro is enabled.
    • The user must also ensure the macro's parent macro-group is enabled.
System information
  • macOS 15.7.5
  • Keyboard Maestro v11.0.4

Run it, and you get a list like this (text)....

...or this (JSON)...

Text output is delimited with the âśż symbol. (I used âśż as the field separator, because I figured it was very unlike to be in either an app or window name.) Or loop through it. Or find a window or app name in the list, etc.

For JSON, go crazy with your favorite JSON methods :).

I'm not sure exactly what I'll use this for, but it's nice knowing I can get an ordered list of windows into Keyboard Maestro. I'll post a couple demo macros soon that use variations on this script to implement two different methods of switching two windows' locations.

-rob.

“Use at your own risk” is of course always sound advice! In the case of AI-generated code, It occurs to me that it might be useful/interesting/reassuring always to know to what extent the code has been checked over. (I am airing just that thought and am not complaining).

Anyway, I shall be safe, because I have at last installed MacOS 15 Sequoia, and find that I can only use one display with my setup now anyway. Thanks to Apple for helping me “unlock my productivity and creativity” (I am not alone, I learn—belatedly!).

Nobody other than me has looked at this code, so essentially nobody, as I know >that much< Swift. However, that's also why I just posted the code separately from the macro, so that it can be looked at without installing a macro.

And in this case, even if you don't know Swift, it's pretty easy to see that the code is quite safe, based on just reading the commands—it doesn't ask for passwords, communicate with the internet, or store anything at all (the output is just print statements).

Now, is it good code? I don't have any idea. But it runs, and it runs well for my purposes, and it lets me do something I couldn't otherwise do.

(Aside: I have a programmer friend who works for a very well known tech company, and they have invested heavily in using AI. He has told me that he can tell when his boss has used AI to help with his code, as it is much better than what the boss was previously writing on his own. I found that an interesting data point.)

Weird; my main Mac is still on Sequoia, and I've got two displays, but I assume your setup must be different than mine, and perhaps somewhat unique? Anyway, that sucks :(.

-rob.

Thanks. I cut down my wording so as to not be verbose, but by “always” I was contemplating what might be good general practice here, and not trying to call you to account in any way.

Just taking this as an example, then: I agree, but I would offer the point that some forum members do not have prior programming experience, whereas you, for one, have lots of it. :wink: So, more generally, someone might say “it looks alright to me, but in need of some tidying up” or “I reckon it’s safe (but don’t sue me)” or “I have no idea what it’s going on here but it seems to work” (:desktop_computer::dog:).

Well, that’s just my opinion, man...

Perhaps it’s worth thinking about for the forum as the use of AI code grows, but either way, as I mentioned, no criticism of your post was implied.

Thanks, it’s not a great problem and there is still some voodoo left to try. It’s down to a combination of which ports the Mac has and which protocols can be used by displays, so yes, setup requirements vary. However, I think the Apple forum discussion that I linked to shows that my situation is far from unique (and indeed at least one article has been written on the subject).

I actually have almost zero real programming experience. I can work my way through simple AppleScripts and shell scripts, but I haven't written an actual full program since 10 Print "Hi!" 20 Goto 10 that many years :).

And as such, given that I can't vouch for any AI-generated code, I make sure it's clear when I'm using some, so people can make their own decisions.

I didn't doubt your word, it's just strange it works for some and not others. Some combination of particular Mac, connection method, and Cupertino anti-magic, I suppose.

-rob.

Well you probably needed a bit of a rest after that!

No, of course not, I was merely making sure that you were aware that was some “evidence” of the problem not being unique to my setup, since hyperlinks can be missed.

I think it’s not so unusual in the world of computer technology. And I wouldn’t be surprised if a lot of Apple software were not tested purely on current examples of Apple hardware (including displays), connected together with Apple cables, and with none of that icky third-party software installed either, come to that! :wink: In practical use, there are variables, of course.

1 Like

Version two is now uploaded in the first post. It's about 3x faster after I left ChatGTP modify Claude's code. It replace all the AppleScript with AXUIElement stuff, and it flies by comparison.

-rob.

FWIW you could also get Swift to return a JSON string, using the NSJSONSerialization Foundation class.

1 Like

Version 3 is now live, with TEXT or JSON as output options. (And this time, I even wrote a tiny little bit of the code myself. Not much, but a few lines :)).

-rob.

1 Like

And last night's optimizations were a bit too ... optimizing, as some windows were skipped. New version of the macro uploaded in the first post.

-rob.