Lich
Lich

Reputation: 491

Swift: Get a list of opened app windows with desktop number and position info

I'm developing a macOS app that would work with app windows. The goal which I couldn't now afford is to retrieve a list of all opened app windows, which would look something like that (JSON example):

[
  {
    "appName": "Google Chrome",
    "appWindows": [
      {
        "size": {"x": 100, "y": 100}, 
        "position": {"height": 100, "width": 100}, 
        "desktopNumber": 2
      },
      {
        "size": {"x": 100, "y": 100}, 
        "position": {"height": 100, "width": 100}, 
        "desktopNumber": 3
      }
    ]
  },
  {
    "appName": "Telegram",
    "appWindows": [
      {
        // full screen app, so has a separate desktop number
        "size": {"x": 1080, "y": 1920}, 
        "position": {"height": 0, "width": 0}, 
        "desktopNumber": 4
      }
    ]
  }
]

Each app has an array of opened windows alongside a desktop number where it's been opened. A full-size app took a separate desktop space, so it should have a separate desktop number.

I know how to get an opened window list with CGWindowListCopyWindowInfo, but I couldn't find a way to find a desktop number or determine which window is visible or not.

If it's not possible to do with Swift, then perhaps there are apple script solutions?

Upvotes: 2

Views: 1924

Answers (1)

Daniel Klöck
Daniel Klöck

Reputation: 21137

You can retrieve app names, bounds & displays using following code:

import Cocoa
import CoreGraphics

let windowsInfo = CGWindowListCopyWindowInfo(CGWindowListOption.optionAll, CGWindowID(0))
let maxDisplays: UInt32 = 10
var displays = [CGDirectDisplayID](repeating: 0, count: Int(maxDisplays))
var displayCount: UInt32 = 0
let error = CGGetOnlineDisplayList(maxDisplays, &displays, &displayCount)

for windowInfo in Array<NSDictionary>.fromCFArray(records: windowsInfo) ?? [] {
    if let appName = windowInfo["kCGWindowOwnerName"] as? String {
        print("appName: \(appName)")
    }
    if let bounds = windowInfo["kCGWindowBounds"] as? NSDictionary {
        print ("X: \(bounds["X"] ?? "<unknown>")")
        print ("Y: \(bounds["Y"] ?? "<unknown>")")
        print ("Width: \(bounds["Width"] ?? "<unknown>")")
        print ("Height: \(bounds["Height"] ?? "<unknown>")")

        if
            let x = bounds["X"] as? Double,
            let y = bounds["Y"] as? Double,
            let width = bounds["Width"] as? Double,
            let height = bounds["Height"] as? Double
        {
            for index in 0..<Int(maxDisplays) {
                let display = displays[index]
                let displayRect = CGDisplayBounds(display)
                if displayRect.contains(CGRect(x: x, y: y, width: width, height: height)) {
                    print("Display: \(index)")
                    break
                }
            }
        }

        print("---")
    }
}

extension Array {
    static func fromCFArray(records: CFArray?) -> Array<Element>? {
        var result: [Element]?
        if let records = records {
            for i in 0..<CFArrayGetCount(records) {
                let unmanagedObject: UnsafeRawPointer = CFArrayGetValueAtIndex(records, i)
                let rec: Element = unsafeBitCast(unmanagedObject, to: Element.self)
                if (result == nil){
                    result = [Element]()
                }
                result!.append(rec)
            }
        }
        return result
    }
}

Example output:

appName: Xcode.app
X: 1284
Y: -1074
Width: 1029
Height: 557
Display: 1
---
appName: Xcode.app
X: 1284
Y: -954
Width: 1029
Height: 261
Display: 1
---
appName: Control Centre
X: 0
Y: 900
Width: 32
Height: 24
---
appName: Control Centre
X: 1294
Y: 0
Width: 146
Height: 24
Display: 0
---
appName: Notes
X: 0
Y: 0
Width: 3200
Height: 24
---
appName: Notes
X: 0
Y: 0
Width: 3200
Height: 24
---
appName: Notes
X: 0
Y: 0
Width: 3200
Height: 24
---
appName: Notes
X: -887
Y: -1333
Width: 3200
Height: 24
Display: 1
---
appName: Notes
X: 0
Y: 0
Width: 1440
Height: 24
Display: 0
---
appName: Notes
X: 0
Y: 0
Width: 1440
Height: 24
Display: 0
---
appName: Notes
X: 0
Y: 0
Width: 1440
Height: 24
Display: 0
---
appName: Notes
X: 0
Y: 0
Width: 1440
Height: 24
Display: 0
---
appName: Notes
X: 0
Y: 0
Width: 1440
Height: 24
Display: 0
---
...

Creating the JSON object with this code is trivial. Btw, you may want to change:

"size": {"x": 1080, "y": 1920}, 
"position": {"height": 0, "width": 0},

to

"position": {"x": 1080, "y": 1920}, 
"size": {"height": 0, "width": 0},

Also, note that apps without a UI may not have a Display value.

Upvotes: 10

Related Questions