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 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.


