Skip to content

Instantly share code, notes, and snippets.

@stephancasas
Last active March 6, 2025 00:03
Show Gist options
  • Save stephancasas/d230ac93d5bc1b0130555e2bc7203fce to your computer and use it in GitHub Desktop.
Save stephancasas/d230ac93d5bc1b0130555e2bc7203fce to your computer and use it in GitHub Desktop.
A convenience type for working with the CFDictionary values returned by CGWindowListCopyWindowInfo(_:, _:)
//
// CGWindowDictionary.swift
//
// Created by Stephan Casas on 11/2/23.
//
import Foundation
import CoreGraphics
import AppKit
typealias CGWindowDictionary = [CFString: Any]
// MARK: - Utilities
/// A convenience type for working with the CFDictionary values returned by `CGWindowListCopyWindowInfo(_:, _:)`.
extension CGWindowDictionary {
/// Get a list of modernized window representations as `[CGWindowDictionary]` using
/// the given constraints.
/// - Parameters:
/// - option: The native window list filter option.
/// - window: The relative window when considering ordered window list filters.
/// - Returns: An array of `CGWindowDictionary` types which match the given parameters.
static func list(
matching option: CGWindowListOption = .optionAll,
relativeTo window: CGWindowID = kCGNullWindowID,
where isIncluded: ((CGWindowDictionary) -> Bool)? = nil
) -> [CGWindowDictionary] {
let windowList = CGWindowListCopyWindowInfo(
option, window
) as? [CGWindowDictionary] ?? []
guard let isIncluded = isIncluded else {
return windowList
}
return windowList.filter(isIncluded)
}
/// Get the frontmost window dictionary for the window which is both on-screen and the
/// frontmost window owned by the given process identifier.
/// - Parameter owner: The window-owning process for which the frontmost window should be resolved.
/// - Returns: The dictionary representation of the frontmost window of the given process.
static func frontmost(for owner: pid_t) -> CGWindowDictionary? {
var windowList = CGWindowDictionary.list(
matching: .optionOnScreenOnly,
where: { $0.processIdentifier == owner })
var frontmost = windowList.first
while windowList.count > 1 {
guard let _frontmost = windowList.first else {
return frontmost
}
frontmost = _frontmost
windowList = CGWindowDictionary.list(
matching: .optionOnScreenAboveWindow,
relativeTo: _frontmost.id,
where: { $0.processIdentifier == owner })
}
return frontmost
}
static func frontmost(for bundleIdentifier: String) -> CGWindowDictionary? {
guard let processIdentifier = NSRunningApplication.runningApplications(
withBundleIdentifier: bundleIdentifier
).first?.processIdentifier else {
return nil
}
return .frontmost(for: processIdentifier)
}
}
// MARK: - Typecast Properties
extension CGWindowDictionary {
/// The window server-tracked window ID for this window.
var id: CGWindowID {
self[kCGWindowNumber] as! CGWindowID
}
/// The process name for the process which owns this window.
var processName: String {
self[kCGWindowOwnerName] as! String
}
/// The process identifier for the process which owns this window.
var processIdentifier: pid_t {
self[kCGWindowOwnerPID] as! pid_t
}
/// The title of this window.
var name: String {
(self[kCGWindowName] as? String) ?? ""
}
/// The window's bounds in the Quartz/CoreGraphics coordinate space — where `(0, 0)` is at the leftmost-lowermost point of the display which owns the space with the lowest ordered index value.
var bounds: CGRect {
.init(dictionaryRepresentation: self[kCGWindowBounds] as! CFDictionary)!
}
/// The layer at which this window draws.
var layer: Int32 {
self[kCGWindowLayer] as! Int32
}
/// The window's bounds in the AppKit coordinate space — where `(0, 0)` is at the leftmost-uppermost point of the display which owns the space with the lowest ordered index value.
var appKitBounds: CGRect {
guard
let NSCGSWindow = objc_getClass(
"NSCGSWindow") as? AnyClass,
let convertRectFromCGCoordinatesPtr = class_getClassMethod(
NSCGSWindow, Selector(("convertRectFromCGCoordinates:")))
else { fatalError(
"Could not load private class method +[NSCGSWindow convertRectFromCGCoordinates:]."
) }
return unsafeBitCast(
method_getImplementation(convertRectFromCGCoordinatesPtr),
to: (@convention(c) (CGRect) -> CGRect).self
)(self.bounds)
}
/// The `AXUIElement` (`AXApplication`) application element representing the owner process of this window.
var axApplication: AXUIElement? {
AXUIElementCreateApplication(self.processIdentifier)
}
/// The *most likely* `AXUIElement` representation of this window.
///
/// There is no *documented* canonical associative value linking `NSWindow` to its `AXUIElement` representation.
/// This extension attempts a best guess based solely on the bounds of the window. Matching further on the window name may, in
/// some cases, reduce the likelihood of collisions but may also contribute to a greater number of false negatives.
var axElement: AXUIElement? {
guard let axApp = self.axApplication else {
return nil
}
let bounds = self.bounds
var windowList: AnyObject?
guard AXUIElementCopyAttributeValue(
axApp, kAXWindowsAttribute as CFString, &windowList
) == .success else {
return nil
}
return (windowList as? [AXUIElement])?.first(where: { windowElement in
var positionValue: AnyObject?
var sizeValue: AnyObject?
guard
AXUIElementCopyAttributeValue(
windowElement, kAXPositionAttribute as CFString, &positionValue) == .success,
AXUIElementCopyAttributeValue(
windowElement, kAXSizeAttribute as CFString, &sizeValue) == .success
else {
return false
}
var position = CGPointZero
var size = CGSizeZero
guard
AXValueGetValue(positionValue as! AXValue, .cgPoint, &position),
AXValueGetValue(sizeValue as! AXValue, .cgSize, &size)
else {
return false
}
return bounds.equalTo(.init(origin: position, size: size))
})
}
/// The bundle identifier for the owner process of this window.
var bundleId: String? {
NSRunningApplication(
processIdentifier: self.processIdentifier
)?.bundleIdentifier
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment