-1

I am writing an application that needs to periodically obtain the PID, process name, window ID and window name of the active window. The program is written in Go, but the issues relate to any FFI into the Mac ecosystem. I have two approaches, via AppleScript and via Objective C.

Both give a 75% solution; either missing the window ID or the window name. I would like to find a single unified solution that gives all four attributes rather than having to cobble together one that depends on both of the approaches that I have.

AppleScript approach; does not provide WindowNumber for most cases.

global activeApp, activePID, activeName, windowName
set windowName to ""
tell application "System Events"
    set activeApp to first application process whose frontmost is true
    set activePID to unix id of activeApp
    set activeName to name of activeApp
    tell process activeName
        try
            tell (1st window whose value of attribute "AXMain" is true)
                set windowName to value of attribute "AXTitle"
            end tell
        end try
    end tell
end tell
return ("{\"pid\":" & activePID & ",\"name\":\"" & activeName & "\",\"window\":\"" & windowName & "\"}" as text)

Objective C approach; does not provide WindowName for most cases.

#include <Cocoa/Cocoa.h>
#include <CoreGraphics/CGWindow.h>

struct details {
   int wid;
   int pid;
   const char* name;
   const char* window;
};

int activeWindow(struct details *d)
{
    if (d == NULL) {
        return 0;
    }
    NSArray *windows = (NSArray *)CGWindowListCopyWindowInfo(kCGWindowListExcludeDesktopElements|kCGWindowListOptionOnScreenOnly,kCGNullWindowID);
    for(NSDictionary *window in windows){
        int WindowLayer = [[window objectForKey:(NSString *)kCGWindowLayer] intValue];
        if (WindowLayer == 0) {
            d->wid = [[window objectForKey:(NSString *)kCGWindowNumber] intValue];
            d->pid = [[window objectForKey:(NSString *)kCGWindowOwnerPID] intValue];
            d->name = [[window objectForKey:(NSString *)kCGWindowOwnerName] UTF8String];
            d->window = [[window objectForKey:(NSString *)kCGWindowName] UTF8String];
            return 1;
        }
    }
    return 0;
}
kortschak
  • 755
  • 5
  • 21
  • Can you give an example where AppleScript returns a name and Objective-C doesn't? – Willeke Apr 30 '23 at 10:44
  • Any iTerm2 window shows the window name with the AppleScript approach but returns a null for the window name via Objective C. – kortschak Apr 30 '23 at 10:54
  • 2
    The [`kCGWindowName` docs](https://developer.apple.com/documentation/coregraphics/kcgwindowname?language=objc) mention that "Note that few applications set the Quartz window name." — running your code on my machine confirms that pretty much no window fetched by this API has a window name accessible this way. AppleScript must be using a different API, possibly Accessibility, to access this information. – Itai Ferber Apr 30 '23 at 13:15
  • Does this answer your question? [Getting Window Number through OSX Accessibility API](https://stackoverflow.com/questions/6178860/getting-window-number-through-osx-accessibility-api) – Willeke May 01 '23 at 09:10
  • I think you need both approaches but the AppleScipt approach can be translated to C using the Accessibility API. – Willeke May 01 '23 at 09:11
  • The window number issue is essentially solved since it's obtained by the Objective C aproach. Would you be able to point to docs for the Accessibility API for this? I have ~0 experience in dev on MacOS. – kortschak May 01 '23 at 20:47

2 Answers2

0

From a Developer forum topic, your Objective-C approach should work if the application has been given Screen Recording permission.

There are a few windows (such as those in menu items) that have weird names, and some just don't have them, but most application and document windows I've tried do have names. There doesn't appear to be an entitlement for Screen Recording, so you would need to direct the user to add the application to the privacy list.

When testing with Xcode 14.1 in macOS Monterey 12.6.5, the kCGWindowName key in the window list dictionary is included when permission has been granted, otherwise it is left out.

red_menace
  • 3,162
  • 2
  • 10
  • 18
  • Thanks, that's very helpful. For other who may be interested, the details are explained in the WWDC19 [talk](https://developer.apple.com/videos/play/wwdc2019/701/) at 16:25 an onwards (transcript: "... CGWindowListCopyWindowInfo never triggers an authorization prompt, instead it filters the set of metadata that it returns to the caller.") – kortschak May 07 '23 at 02:19
  • If you allow third-party apps or websites to record your screen, any information they collect is governed by their terms and privacy policies. It’s recommended that you learn about the privacy practices of those parties. – Robert Kniazidis May 07 '23 at 05:54
  • @RobertKniazidis - Exactly. I don’t know how many actually wade though all those developer videos (I certainly don’t), but I think it would be easy to miss a small detail (from 4 years ago) that unlike many of the privacy settings, `CGWindowListCopyWindowInfo` never triggers an authorization prompt, and expects the app to be preapproved for screen recording in order to to read the names of windows (other than its own). – red_menace May 07 '23 at 06:43
-1
tell application "System Events"
    set frontProcess to 1st process whose frontmost is true
    set activePID to unix id of frontProcess
    set bundleID to bundle identifier of frontProcess
    set activeName to name of frontProcess
end tell

tell application id bundleID
    try
        set appName to name
        set windowID to id of front window
        set windowName to name of front window
    on error
        set windowID to my getFrontWindowID(appName)
        try
            tell application "System Events" to tell frontProcess to tell window 1 to set windowName to value of attribute "AXTitle"
        on error
            set windowName to ""
        end try
    end try
end tell

return {pid:activePID, processName:activeName, appName:appName, windowID:windowID, windowName:windowName}
--> {pid:9893, processName:"Avidemux2.7", appName:"Avidemux_2.7.8", windowID:5969, windowName:"Avidemux"}


on getFrontWindowID(appName)
    set JS to "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 => [x.kCGWindowNumber]);           
}
Application('" & appName & "').getWindowList();"
    return (word 1 of (do shell script "osascript -l JavaScript -e " & quoted form of JS)) as integer
end getFrontWindowID
Robert Kniazidis
  • 1,760
  • 1
  • 7
  • 8
  • 1
    I think this falls into the realms of cobbled together. Not to diminish the effort, I would rather do an FFI call to the Objective C from Go and if the name is not present, then shell out of `osascript`. This would be a less than or equal to single shell-out rather than less than or equal to two for each query. – kortschak May 01 '23 at 08:02
  • 1
    It doesn't solve the problem as asked, so I have to say that even though it would function, it doesn't work for me. The question, if treated as a spec, requires that there be a single unified approach. Perhaps I should have defined "single unified" more clearly. The code that I have at the moment makes an FFI call to Objective-C and then if there is no window name in the result, shells out to `osascript` to get that. What is here requires that I shell out to `osascript` and then the script may make another shell-out. "I don't care about your claims."? OK – kortschak May 01 '23 at 20:44
  • Re: @kortschak, Good luck finding a "unified solution" as you understand it. It doesn't seem to use one of the 2 (AppleScript, which retrieves the window's name, or C code, which retrieves its ID), but magically extracts both. Also, I wish you to learn the magic word "thanks" for the user's attempt to provide help. – Robert Kniazidis May 02 '23 at 13:14