How to Get the List of Open Windows on MACos in Swift

How to get the list of open windows on MacOS in Swift?

It seems that all visible windows have the value 0 for key kCGWindowLayer

import Cocoa

let options = CGWindowListOption(arrayLiteral: .excludeDesktopElements, .optionOnScreenOnly)
let windowsListInfo = CGWindowListCopyWindowInfo(options, CGWindowID(0))
let infoList = windowsListInfo as! [[String:Any]]
let visibleWindows = infoList.filter{ $0["kCGWindowLayer"] as! Int == 0 }

print(visibleWindows)

List all window names in Swift

It becomes easier if you avoid using the Core Foundation types and methods, and bridge the values to native Swift types as early as possible.

Here, CGWindowListCopyWindowInfo() returns an optional CFArray of CFDictionaries, and that can be bridged to the corresponding Swift type [[String : Any]]. Then you can access its values with the usual Swift methods (array enumeration and dictionary subscripting):

if let windowInfo = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) as? [[ String : Any]] {
for windowDict in windowInfo {
if let windowName = windowDict[kCGWindowName as String] as? String {
print(windowName)
}
}
}

How to get a list of all open NSWindow from all running application?

Note that not all windows are necessarily NSWindows, and that NSWindow only provides an interface to windows in your own address space.

The supported way to access every window is the CGWindow API. Take a look at the Son of Grab sample code to see how it's done.

How to get current active window using Swift and Cocoa

So, with the comments from Alexander and Wileke I think I found a solution.

With NSWorkspace.shared.frontmostApplication you can check, if Safari is currently active.
But as Wileke noted, that does not mean, that it has an active window.
Therefore, we use CGWindowListCopyWindowInfo to first get all windows and check if at least one of them belongs to Safari by comparing the PIDs.

This way, we can safely say that Safari must have currently an active window that receives key events.
This must be true as there is now way that Safari is front most without any windows or that Safari as an window but is not front most at the same time.

Well, unless I missed something.
But for now it works.

Here is code I came up with:

func safariIsActive() -> Bool {
// Get the app that currently has the focus.
let frontApp = NSWorkspace.shared.frontmostApplication!

// Check if the front most app is Safari
if frontApp.bundleIdentifier == "com.apple.Safari" {
// If it is Safari, it still does not mean, that is receiving key events
// (i.e., has a window at the front).
// But what we can safely say is, that if Safari is the front most app
// and it has at least one window, it has to be the window that
// crrently receives key events.
let safariPID = frontApp.processIdentifier

// With this procedure, we get all available windows.
let options = CGWindowListOption(arrayLiteral: CGWindowListOption.excludeDesktopElements, CGWindowListOption.optionOnScreenOnly)
let windowListInfo = CGWindowListCopyWindowInfo(options, CGWindowID(0))
let windowInfoList = windowListInfo as NSArray? as? [[String: AnyObject]]

// Now that we have all available windows, we are going to check if at least one of them
// is owned by Safari.
for info in windowInfoList! {
let windowPID = info["kCGWindowOwnerPID"] as! UInt32
if windowPID == safariPID {
return true
}
}
}
return false
}

How to get window list from core-grapics API with swift

  • kCGWindowListOptionOnScreenOnly is an Int, you have to convert
    that to CGWindowListOption aka UInt32.
  • The C definition

    #define kCGNullWindowID ((CGWindowID)0) 

    is not imported into Swift, therefore you have to use the constant
    0.

  • In addition, CGWindowListCopyWindowInfo() returns an Unmanaged<CFArray>!, therefore you have to call takeRetainedValue()
    on the returned value (as documented in "Working with Cocoa Data Types").

Together:

let option = CGWindowListOption(kCGWindowListOptionOnScreenOnly)
let relativeToWindow = CGWindowID(0)
let info = CGWindowListCopyWindowInfo(option, relativeToWindow).takeRetainedValue()

Then you can enumerate this array of dictionaries with

for dict in info as! [ [ String : AnyObject] ] {
// ...
}

Update for Swift 3:

if let info = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) as? [[ String : Any]] {
for dict in info {
// ...
}
}

Swift: How to activate and unhide window of ANY application?

Here is working Playground module. The approach is to use KVO for observable properties to be informed when exactly desired state for target application occurs. Hope it would be helpful somehow.

import Cocoa

class AppActivator: NSObject {

private var application: NSRunningApplication!
private let filterName: String

init(appName: String) {
filterName = appName
}

func activate() {
guard let app = NSWorkspace.shared.runningApplications.filter ({
return $0.localizedName == self.filterName || $0.bundleIdentifier?.contains(self.filterName) ?? false
}).first else {
print("Application \(self.filterName) not found")
return
}

guard app.activationPolicy != .prohibited else {
print("Application \(self.filterName) prohibits activation")
return
}

self.application = app

self.unhideAppIfNeeded()
self.activateAppIfNeeded()
}

private func unhideAppIfNeeded() {
if application.isHidden {
application.addObserver(self, forKeyPath: "isHidden", options: .new, context: nil)
application.unhide()
}
}

private func activateAppIfNeeded() {
if !application.isHidden && !application.isActive {
application.addObserver(self, forKeyPath: "isActive", options: .new, context: nil)
application.activate(options: .activateIgnoringOtherApps)
}
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "isHidden" {
application.removeObserver(self, forKeyPath: "isHidden")
activateAppIfNeeded()
} else if keyPath == "isActive" {
application.removeObserver(self, forKeyPath: "isActive")
print("Application \(application.localizedName) - ACTIVATED!")
}
}
}

let activator = AppActivator(appName: "Finder")
activator.activate()

Get list of all open windows in .Net Core running on macOS (via NSApplication?)

In the link you refer to, there is an important note:

... as Xamarin.Mac.dll does not run under the .NET Core runtime, it only runs with the Mono runtime.

Because you try to run Xamarin.Mac.dll under .net-core, you get this dlopen error.

No System-wide List via NSApplication

The linked answer with NSApplication.shared.windows is incorrect if you want to read a system-wide list of open windows. It can only be used to determine all currently existing windows for the application from which the call is made, see Apple's documentation.

Alternative solution

Nevertheless, there are several ways to access the Window information in macOS. One of them could be a small unmanaged C-lib that gets the necessary information via CoreFoundation and CoreGraphics and returns it to C# via Platform Invoke (P/Invoke).

Native Code

Here is example code for a C-Lib that determines and returns the names of the window owners.

WindowsListLib.h

extern char const **windowList(void);
extern void freeWindowList(char const **list);

The interface of the library consists of only two functions. The first function called windowList returns a list with the names of the window owners. The last element of the list must be NULL so that you can detect where the list ends on the managed C# side. Since the memory for the string list is allocated dynamically, you must use the freeWindowList function to free the associated memory after processing.

WindowsListLib.c

#include "WindowListLib.h"
#include <CoreFoundation/CoreFoundation.h>
#include <CoreGraphics/CoreGraphics.h>

static void errorExit(char *msg) {
fprintf(stderr, "%s\n", msg);
exit(1);
}

static char *copyUTF8String(CFStringRef string) {
CFIndex length = CFStringGetLength(string);
CFIndex size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
char *buf = malloc(size);
if(!buf) {
errorExit("malloc failed");
}
if(!CFStringGetCString(string, buf, size, kCFStringEncodingUTF8)) {
errorExit("copyUTF8String with utf8 encoding failed");
}
return buf;
}

char const **windowList(void) {
CFArrayRef cfWindowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
CFIndex count = CFArrayGetCount(cfWindowList);
char const **list = malloc(sizeof(char *) * (count + 1));
if(!list) {
errorExit("malloc failed");
}
list[count] = NULL;
for(CFIndex i = 0; i < count; i++) {
CFDictionaryRef windowInfo = CFArrayGetValueAtIndex(cfWindowList, i);
CFStringRef name = CFDictionaryGetValue(windowInfo, kCGWindowOwnerName);
if(name) {
list[i] = copyUTF8String(name);
} else {
list[i] = strdup("unknown");
}
}
CFRelease(cfWindowList);
return list;
}

void freeWindowList(char const **list) {
const char **ptr = list;
while(*ptr++) {
free((void *)*ptr);
}
free(list);
}

CGWindowListCopyWindowInfo is the actual function that gets the window information. It returns a list of dictionaries containing the details. From this we extract kCGWindowOwnerName. This CFStringRef is converted to a dynamically allocated UTF-8 string by the function copyUTF8String.

By convention, calls like CGWindowListCopyWindowInfo that contain the word copy (or create) must be released after use with CFRelease to avoid creating memory leaks.

C# Code

The whole thing can then be called on the C# side something like this:

using System.Runtime.InteropServices;

namespace WindowList
{
public static class Program
{
[DllImport("WindowListLib", EntryPoint = "windowList")]
private static extern IntPtr WindowList();

[DllImport("WindowListLib", EntryPoint = "freeWindowList")]
private static extern void FreeWindowList(IntPtr list);

private static List<string> GetWindows()
{
var nativeWindowList = WindowList();
var windows = new List<string>();
var nativeWindowPtr = nativeWindowList;
string? windowName;
do
{
var strPtr = Marshal.ReadIntPtr(nativeWindowPtr);
windowName = Marshal.PtrToStringUTF8(strPtr);
if (windowName == null) continue;
windows.Add(windowName);
nativeWindowPtr += Marshal.SizeOf(typeof(IntPtr));
} while (windowName != null);

FreeWindowList(nativeWindowList);
return windows;
}

static void Main()
{
foreach (var winName in GetWindows())
{
Console.WriteLine(winName);
}
}
}
}

The GetWindows method fetches the data via a native call to WindowList and converts the C strings to managed strings, then releases the native resources via a call to FreeWindowList.

This function returns only the owner names, such as Finder, Xcode, Safari, etc. If there are multiple windows, the owners will also be returned multiple times, etc. The exact logic of what should be determined will probably have to be changed according to your requirements. However, the code above should at least show a possible approach to how this can be done.

Screenshot

demo



Related Topics



Leave a reply



Submit