Last active
March 6, 2025 00:03
-
-
Save stephancasas/d230ac93d5bc1b0130555e2bc7203fce to your computer and use it in GitHub Desktop.
A convenience type for working with the CFDictionary values returned by CGWindowListCopyWindowInfo(_:, _:)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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