Getting Window ID on Mac
Why
In my case I wanted to make an automation for OBS. The best way for me to share a window is to make a macOS Screen Capture source and chose there Window Capture method. The goal was to find the ID, which changes on reopening. The journey starts with CGWindowListCopyWindowInfo, but it takes unexpected turn. At least for me.
How
So like everyone else in need to do something new, I started with researching the web and pretty fast I have found a convenient method: CGWindowListCopyWindowInfo. The problem is that all the sources described with a little detail. And all the code seemed to be outdated: it is either Objective C (I have nothing against it, I love it) or earlier versions of Swift (about 2 and 3), which doesn’t work via copy-paste, and Xcode doesn’t provide ways to convert it. So I turned to the source of truth, the Apple documentation! One can use AI tools and whatnot, I did not[1].
After a little while I collected some basic knowledge on the topic, and started with SPM
swift package init --name FindWindowId --type executable
Now to the code, the most common sample looks like this one:
CFArrayRef windowArray = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
NSMutableArray *windowsInMap = [NSMutableArray arrayWithCapacity:64];
NSArray* windows = (NSArray*)CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
NSUInteger count = [windows count];
for (NSUInteger i = 0; i < count; i++)
{
NSDictionary* nswindowsdescription = [windows objectAtIndex:i];
NSNumber* windowid = (NSNumber*)[nswindowsdescription objectForKey:@"kCGWindowNumber"];
if(windowid)
{
// Entries will be in front to back order.
}
}
CFRelease(windowArray);
After a little while I ended up with something similar to this (who knew that it would be so easy to use core nowadays):
let windowList = CGWindowListCopyWindowInfo([.optionOnScreenOnly], .zero) as? [[String: AnyObject]]
for window in windowList {
if let windowNumber = window["kCGWindowNumber"] as? Int,
let windowName = window["kCGWindowName"] as? String {
print("\(windowName): \(windowNumber)")
}
}
This newfound solution compiles, it runs, and somewhat works, but only a fraction of windows is visible and the list doesn’t look like something usable.
StatusIndicator: 938
StatusIndicator: 937
StatusIndicator: 936
Menubar: 386
Menubar: 375
Wallpaper-24ACAB3E-24DF-43F3-A508-8FF0C7A88DC2: 374
Wallpaper-: 373
The main question in my head is where are all the real windows you can see on your display? How to access them? After a little of research I found the way to find Window IDs of all available applications:
print(NSWindow.windowNumbers(options: [.allApplications, .allSpaces]))
Easy! But with this I can only see IDs with no additional information, no window names, so I cannot identify the required window by the name or any other information.
This. Right here. This is the place where the fun begins for me. The information is there, but how to access it?
Next step
I continued my research and found that you need to have accessibility rights to access windows of other applications. Makes sense. Okay, not a big problem, it is quite easy to check for accessibility rights:
if !AXIsProcessTrusted() {
print("Please add this app in System Settings → Privacy & Security → Accessibility")
exit(1)
}
Once I ran it, I went to the System Settings and there is nowhere to see my executable in the list of apps requested to access the accessibility information. It should appear there automatically, but what do you think, of course I added my binary manually to the list, and nothing changed. My process was still not trusted.
I tried to codesign the executable:
swift build -c release
codesign --sign "Apple Development" --options runtime --timestamp .build/release/FindWindowId
I tried to embed the Info.plist into the binary:
let package = Package(
name: "FindWindowId",
platforms: [.macOS(.v26)],
targets: [
.executableTarget(
name: "FindWindowId",
linkerSettings: [
.unsafeFlags([
"-Xlinker", "-sectcreate",
"-Xlinker", "__TEXT",
"-Xlinker", "__info_plist",
"-Xlinker", "Sources/Resources/Info.plist"
])
]
),
]
)
But I failed. I know it is possible, I will return to this at some point, but I want to have a working tool about right away, so I started the process from the beginning and created a new Xcode project with macOS executable as a target.
This time it works automagically, the application appears in the Privacy & Security → Accessibility by itself and you just need to toggle the button. Yay.
AXIsProcessTrusted() now returns true, but… Window IDs are nowhere to find. Sic.
The real how
This is where I’ve spent the most amount of time, reading Apple documentation, trying to figure out the proper prompt for a search engine (not Google, of course). Now, when I look back at the problem and think about all the applications which can access Window IDs, I understand that the answer was floating somewhere nearby, but it all was hidden in the muddied waters of other features of those applications.
To me it as very surprizing to find that in order to obtain Window IDs you need to ask for screen recording permissions.
if !CGPreflightScreenCaptureAccess() {
if CGRequestScreenCaptureAccess() {
print("Please approve screen recording and rerun the application")
exit(1)
}
}
TL;RD
- You need to sign your app properly, with Xcode project it is easier to solve (SPM solution is yet to be found for me).
- Check that you have accessibility access right with
AXIsProcessTrusted(). - Ensure you requested recording permissions with
CGRequestScreenCaptureAccess(). You can check if your app has them later on withCGPreflightScreenCaptureAccess().
The resulting code could look like this (naive implementation):
import Foundation
import Cocoa
checkAccessRights()
let windowName = getWindowNameFromArguments()
print(findWindowId(withName: windowName, from: getWindowList()))
func checkAccessRights() {
var isMissingPermission = false
if !AXIsProcessTrusted() {
print("Please add this app in System Settings -> Privacy & Security -> Accessibility")
isMissingPermission = true
}
if !CGPreflightScreenCaptureAccess() {
if CGRequestScreenCaptureAccess() {
print("Please approve screen recording and rerun the application")
isMissingPermission = true
}
}
if isMissingPermission {
exit(1)
}
}
func getWindowNameFromArguments() -> String {
guard CommandLine.argc == 2 else {
print("You need to pass exactly 1 argument: the window name")
exit(1)
}
return CommandLine.arguments[1]
}
func findWindowId(withName name: String, from windowList: [[String: AnyObject]]) -> Int {
for window in windowList {
if let windowNumberString = window["kCGWindowNumber"] as? Int,
let windowNameString = window["kCGWindowName"] as? String,
windowNameString == name {
return windowNumberString
}
}
return -1
}
func getWindowList() -> [[String: AnyObject]] {
guard let windowList = CGWindowListCopyWindowInfo([.optionOnScreenOnly], .init(1)) as? [[String: AnyObject]] else {
print("Error getting window list")
exit(1)
}
return windowList
}
The most important thing, it is finally working properly, I can find the needed Window ID and automate my workflow.
[1]: Yes, they could help to build the tool easier, but will you learn and understand more? Will you be able to do something similar in the future? Will you know where and how to look? I know, it is a hot topic. For my own projects I tend to use as little helpers as possible. Yes, it is much slower, but in the process I learn something and my brain is being trained. I do not want to have so called cognitive debt, so I read the official documentation and go slowly. Like in a brain gym. ↩