import AppKit
|
@preconcurrency import ApplicationServices
|
import CoreGraphics
|
import Foundation
|
import AlignerCore
|
|
final class CGWindowAXWindowService: WindowServiceProtocol, WindowRestorationServiceProtocol, WindowActivationServiceProtocol, WindowCloseServiceProtocol {
|
weak var delegate: WindowServiceDelegate?
|
|
private let appCategorizer: any AppCategorizer
|
private let spaceIDsByWindowIDProvider: ([UInt32]) -> [UInt32: [UInt64]]
|
private let fullscreenSpaceIDs: Set<UInt64>
|
private let privateActivationBridge: PrivateWindowActivationBridge?
|
private let privateSpaceActivationBridge: PrivateSpaceActivationBridge?
|
private var restorableAXWindowsByID: [UInt32: RestorableAXWindow] = [:]
|
|
init(
|
spaceIDsByWindowIDProvider: @escaping ([UInt32]) -> [UInt32: [UInt64]],
|
appCategorizer: any AppCategorizer = BundleIDAppCategorizer(),
|
fullscreenSpaceIDs: Set<UInt64> = [],
|
privateActivationBridge: PrivateWindowActivationBridge? = .shared,
|
privateSpaceActivationBridge: PrivateSpaceActivationBridge? = .shared
|
) {
|
self.appCategorizer = appCategorizer
|
self.fullscreenSpaceIDs = fullscreenSpaceIDs
|
self.spaceIDsByWindowIDProvider = spaceIDsByWindowIDProvider
|
self.privateActivationBridge = privateActivationBridge
|
self.privateSpaceActivationBridge = privateSpaceActivationBridge
|
}
|
|
func allWindows() throws -> [AlignerWindow] {
|
let records = try windowRecords()
|
return records.compactMap(WindowEnumerationPolicy.window)
|
}
|
|
func windows(for space: AlignerSpace) throws -> [AlignerWindow] {
|
try allWindows().filter { window in
|
window.spaceIDs.contains(space.id)
|
}
|
}
|
|
func refresh() {}
|
|
func startObserving() {}
|
|
func stopObserving() {}
|
|
func restore(windowID: UInt32) throws -> WindowRestoreResult {
|
guard let restorableWindow = restorableAXWindowsByID[windowID] else {
|
DevelopmentDiagnostics.log("windowActivation.restore.windowNotFound", [
|
"windowID": windowID
|
])
|
return .windowNotFound
|
}
|
DevelopmentDiagnostics.log("windowActivation.restore.start", [
|
"windowID": windowID,
|
"pid": restorableWindow.processIdentifier
|
])
|
|
let minimizeResult = AXUIElementSetAttributeValue(
|
restorableWindow.element,
|
kAXMinimizedAttribute as CFString,
|
kCFBooleanFalse
|
)
|
guard minimizeResult == .success else {
|
DevelopmentDiagnostics.log("windowActivation.restore.setMinimizedFailed", [
|
"windowID": windowID,
|
"pid": restorableWindow.processIdentifier,
|
"axError": String(describing: minimizeResult)
|
])
|
throw AXWindowRestoreError.setMinimizedFailed(minimizeResult)
|
}
|
|
let focusedBeforeActivateResult = focusWindowResult(
|
restorableWindow.element,
|
processIdentifier: restorableWindow.processIdentifier
|
)
|
let raisedBeforeActivateResult = raiseWindowResult(restorableWindow.element)
|
let focusedBeforeActivate = focusedBeforeActivateResult.success
|
let raisedBeforeActivate = raisedBeforeActivateResult.success
|
|
guard activateApplication(processIdentifier: restorableWindow.processIdentifier) else {
|
DevelopmentDiagnostics.log("windowActivation.restore.activateApplicationFailed", [
|
"windowID": windowID,
|
"pid": restorableWindow.processIdentifier,
|
"focusedBeforeActivate": focusedBeforeActivate,
|
"focusBeforeAXError": String(describing: focusedBeforeActivateResult.error),
|
"raisedBeforeActivate": raisedBeforeActivate,
|
"raiseBeforeAXError": String(describing: raisedBeforeActivateResult.error)
|
])
|
return .activationFailed
|
}
|
|
let focusedAfterActivateResult = focusWindowResult(
|
restorableWindow.element,
|
processIdentifier: restorableWindow.processIdentifier
|
)
|
let raisedAfterActivateResult = raiseWindowResult(restorableWindow.element)
|
let focusedAfterActivate = focusedAfterActivateResult.success
|
let raisedAfterActivate = raisedAfterActivateResult.success
|
|
let restored = focusedBeforeActivate || focusedAfterActivate || raisedBeforeActivate || raisedAfterActivate
|
DevelopmentDiagnostics.log("windowActivation.restore.end", [
|
"windowID": windowID,
|
"pid": restorableWindow.processIdentifier,
|
"focusedBeforeActivate": focusedBeforeActivate,
|
"focusBeforeAXError": String(describing: focusedBeforeActivateResult.error),
|
"raisedBeforeActivate": raisedBeforeActivate,
|
"raiseBeforeAXError": String(describing: raisedBeforeActivateResult.error),
|
"focusedAfterActivate": focusedAfterActivate,
|
"focusAfterAXError": String(describing: focusedAfterActivateResult.error),
|
"raisedAfterActivate": raisedAfterActivate,
|
"raiseAfterAXError": String(describing: raisedAfterActivateResult.error),
|
"result": restored ? "restored" : "activationFailed"
|
])
|
return focusedBeforeActivate || focusedAfterActivate || raisedBeforeActivate || raisedAfterActivate
|
? .restored
|
: .activationFailed
|
}
|
|
func activate(window: AlignerWindow) throws -> WindowActivationResult {
|
DevelopmentDiagnostics.log("windowActivation.activate.start", [
|
"windowID": window.id,
|
"identifierSource": String(describing: window.identifierSource),
|
"titleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
|
"titleLength": window.title.count,
|
"titleIsEmpty": window.title.isEmpty,
|
"appName": window.app.name,
|
"bundleIdentifier": window.app.bundleIdentifier,
|
"pid": window.app.processIdentifier,
|
"isMinimized": window.isMinimized,
|
"isFullscreen": window.isFullscreen
|
])
|
if window.isMinimized {
|
let refreshedWindows = try allWindows()
|
DevelopmentDiagnostics.log("windowActivation.activate.refreshedBeforeRestore", [
|
"windowID": window.id,
|
"refreshedWindowCount": refreshedWindows.count
|
])
|
let restoreResult = try restore(windowID: window.id)
|
DevelopmentDiagnostics.log("windowActivation.activate.restoreResult", [
|
"windowID": window.id,
|
"result": Self.restoreResultString(restoreResult)
|
])
|
switch restoreResult {
|
case .restored:
|
return .restored
|
case .windowNotFound:
|
return activateApplication(for: window) ? .activationFailed : .windowNotFound
|
case .unsupported:
|
return .unsupported
|
case .activationFailed:
|
return .activationFailed
|
}
|
}
|
|
let spaceFocusOutcome = focusTargetSpaceIfNeeded(for: window)
|
if spaceFocusOutcome?.didRequestFocus == true {
|
Thread.sleep(forTimeInterval: 0.12)
|
}
|
|
let privateActivationOutcome = activateViaPrivateWindowServerAPI(window)
|
let didPrivatelyActivate = privateActivationOutcome?.succeeded == true
|
|
let axWindow = axWindow(
|
for: window,
|
in: AXWindowMetadataReader.metadata(appCategorizer: appCategorizer),
|
operation: "windowActivation",
|
matchAttempt: "initial"
|
)
|
|
DevelopmentDiagnostics.log("windowActivation.activate.axLookup", [
|
"windowID": window.id,
|
"titleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
|
"titleLength": window.title.count,
|
"titleIsEmpty": window.title.isEmpty,
|
"pid": window.app.processIdentifier,
|
"found": axWindow != nil,
|
"matchedWindowID": axWindow?.windowID,
|
"matchedTitleHash": DevelopmentDiagnostics.stableFingerprint(axWindow?.title),
|
"matchedTitleLength": axWindow?.title?.count,
|
"matchedTitleIsEmpty": axWindow?.title?.isEmpty,
|
"matchedPID": axWindow?.processIdentifier,
|
"spaceFocusTargetSpaceID": spaceFocusOutcome?.targetSpaceID,
|
"spaceFocusDisplayIdentifier": spaceFocusOutcome?.displayIdentifier,
|
"spaceFocusPreviousSpaceID": spaceFocusOutcome?.previousCurrentSpaceID,
|
"spaceFocusRequested": spaceFocusOutcome?.didRequestFocus,
|
"spaceFocusError": spaceFocusOutcome?.error
|
])
|
guard let axWindow else {
|
let activated = activateApplication(for: window)
|
DevelopmentDiagnostics.log("windowActivation.activate.axMissing", [
|
"windowID": window.id,
|
"appName": window.app.name,
|
"pid": window.app.processIdentifier,
|
"privateActivated": didPrivatelyActivate,
|
"applicationActivated": activated,
|
"result": didPrivatelyActivate ? "activated" : (activated ? "appActivatedOnly" : "windowNotFound")
|
])
|
if didPrivatelyActivate {
|
return .activated
|
}
|
|
return activated ? .appActivatedOnly : .windowNotFound
|
}
|
|
let focusedBeforeActivateResult = focusWindowResult(
|
axWindow.element,
|
processIdentifier: axWindow.processIdentifier
|
)
|
let raisedBeforeActivateResult = raiseWindowResult(axWindow.element)
|
let focusedBeforeActivate = focusedBeforeActivateResult.success
|
let raisedBeforeActivate = raisedBeforeActivateResult.success
|
|
guard didPrivatelyActivate || activateApplication(for: window) else {
|
DevelopmentDiagnostics.log("windowActivation.activate.applicationActivationFailed", [
|
"windowID": window.id,
|
"pid": window.app.processIdentifier,
|
"privateActivated": didPrivatelyActivate,
|
"focusedBeforeActivate": focusedBeforeActivate,
|
"focusBeforeAXError": String(describing: focusedBeforeActivateResult.error),
|
"raisedBeforeActivate": raisedBeforeActivate,
|
"raiseBeforeAXError": String(describing: raisedBeforeActivateResult.error)
|
])
|
return .activationFailed
|
}
|
|
let focusedAfterActivateResult = focusWindowResult(
|
axWindow.element,
|
processIdentifier: axWindow.processIdentifier
|
)
|
let raisedAfterActivateResult = raiseWindowResult(axWindow.element)
|
let focusedAfterActivate = focusedAfterActivateResult.success
|
let raisedAfterActivate = raisedAfterActivateResult.success
|
|
let didFocusOrRaise = focusedBeforeActivate || focusedAfterActivate || raisedBeforeActivate || raisedAfterActivate
|
DevelopmentDiagnostics.log("windowActivation.activate.end", [
|
"windowID": window.id,
|
"pid": window.app.processIdentifier,
|
"privateActivated": didPrivatelyActivate,
|
"focusedBeforeActivate": focusedBeforeActivate,
|
"focusBeforeAXError": String(describing: focusedBeforeActivateResult.error),
|
"raisedBeforeActivate": raisedBeforeActivate,
|
"raiseBeforeAXError": String(describing: raisedBeforeActivateResult.error),
|
"focusedAfterActivate": focusedAfterActivate,
|
"focusAfterAXError": String(describing: focusedAfterActivateResult.error),
|
"raisedAfterActivate": raisedAfterActivate,
|
"raiseAfterAXError": String(describing: raisedAfterActivateResult.error),
|
"result": (didFocusOrRaise || didPrivatelyActivate) ? "activated" : "appActivatedOnly"
|
])
|
return didPrivatelyActivate || focusedBeforeActivate || focusedAfterActivate || raisedBeforeActivate || raisedAfterActivate
|
? .activated
|
: .appActivatedOnly
|
}
|
|
func close(window: AlignerWindow) throws -> WindowCloseResult {
|
DevelopmentDiagnostics.log("windowClose.window.start", [
|
"windowID": window.id,
|
"identifierSource": String(describing: window.identifierSource),
|
"titleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
|
"titleLength": window.title.count,
|
"titleIsEmpty": window.title.isEmpty,
|
"appName": window.app.name,
|
"bundleIdentifier": window.app.bundleIdentifier,
|
"pid": window.app.processIdentifier
|
])
|
|
let axMetadata = AXWindowMetadataReader.metadata(appCategorizer: appCategorizer)
|
var matchedAXWindow = axWindow(for: window, in: axMetadata)
|
var finalAXMetadata = axMetadata
|
|
let shouldPreferFocusedPageClose = matchedAXWindow?.windowID != window.id
|
if shouldPreferFocusedPageClose,
|
let focusedPageCloseResult = closeFocusedPageViaCommandW(
|
window: window,
|
reason: matchedAXWindow == nil ? "axMissing" : "nonDirectAXMatch"
|
) {
|
return focusedPageCloseResult
|
}
|
|
if matchedAXWindow == nil {
|
Thread.sleep(forTimeInterval: 0.12)
|
finalAXMetadata = AXWindowMetadataReader.metadata(appCategorizer: appCategorizer)
|
matchedAXWindow = axWindow(for: window, in: finalAXMetadata, matchAttempt: "retry")
|
}
|
|
guard let matchedAXWindow else {
|
DevelopmentDiagnostics.log("windowClose.window.axMissing", [
|
"windowID": window.id,
|
"appName": window.app.name,
|
"pid": window.app.processIdentifier,
|
"candidateCount": finalAXMetadata.filter { $0.processIdentifier == window.app.processIdentifier }.count,
|
"matchingTitleCandidateCount": matchingAXTitleCandidateCount(for: window, in: finalAXMetadata),
|
"matchingSizeCandidateCount": matchingAXSizeCandidateCount(for: window, in: finalAXMetadata)
|
])
|
return .windowNotFound
|
}
|
|
if matchedAXWindow.windowID != window.id,
|
let focusedPageCloseResult = closeFocusedPageViaCommandW(
|
window: window,
|
reason: "retryNonDirectAXMatch"
|
) {
|
return focusedPageCloseResult
|
}
|
|
guard let closeButton = closeButton(for: matchedAXWindow) else {
|
Thread.sleep(forTimeInterval: 0.12)
|
let retryMetadata = AXWindowMetadataReader.metadata(appCategorizer: appCategorizer)
|
if let retryAXWindow = axWindow(for: window, in: retryMetadata, matchAttempt: "closeButtonRetry"),
|
let retryCloseButton = closeButton(for: retryAXWindow) {
|
return performCloseButtonPress(
|
retryCloseButton,
|
window: window,
|
processIdentifier: retryAXWindow.processIdentifier,
|
attempt: "closeButtonRetry"
|
)
|
}
|
DevelopmentDiagnostics.log("windowClose.window.closeButtonMissing", [
|
"windowID": window.id,
|
"pid": matchedAXWindow.processIdentifier,
|
"appName": window.app.name
|
])
|
return .windowNotFound
|
}
|
|
return performCloseButtonPress(
|
closeButton,
|
window: window,
|
processIdentifier: matchedAXWindow.processIdentifier,
|
attempt: "initial"
|
)
|
}
|
|
private func closeButton(for axWindow: AXWindowMetadata) -> AXUIElement? {
|
var closeButtonValue: CFTypeRef?
|
let closeButtonLookupResult = AXUIElementCopyAttributeValue(
|
axWindow.element,
|
kAXCloseButtonAttribute as CFString,
|
&closeButtonValue
|
)
|
guard closeButtonLookupResult == .success,
|
let closeButton = closeButtonValue,
|
CFGetTypeID(closeButton) == AXUIElementGetTypeID()
|
else {
|
DevelopmentDiagnostics.log("windowClose.window.closeButtonLookupFailed", [
|
"pid": axWindow.processIdentifier,
|
"axError": String(describing: closeButtonLookupResult)
|
])
|
return nil
|
}
|
|
return (closeButton as! AXUIElement)
|
}
|
|
private func performCloseButtonPress(
|
_ closeButton: AXUIElement,
|
window: AlignerWindow,
|
processIdentifier: Int32,
|
attempt: String
|
) -> WindowCloseResult {
|
let closeResult = AXUIElementPerformAction(closeButton, kAXPressAction as CFString)
|
DevelopmentDiagnostics.log("windowClose.window.result", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"attempt": attempt,
|
"axError": String(describing: closeResult)
|
])
|
|
switch closeResult {
|
case .success:
|
return .requested
|
case .actionUnsupported, .notImplemented:
|
return .unsupported
|
default:
|
return .failed(String(describing: closeResult))
|
}
|
}
|
|
private func closeFocusedPageViaCommandW(
|
window: AlignerWindow,
|
reason: String
|
) -> WindowCloseResult? {
|
guard window.identifierSource == .cgWindow,
|
let processIdentifier = window.app.processIdentifier
|
else {
|
DevelopmentDiagnostics.log("windowClose.commandW.skipped", [
|
"windowID": window.id,
|
"reason": reason,
|
"identifierSource": String(describing: window.identifierSource),
|
"hasPID": window.app.processIdentifier != nil
|
])
|
return nil
|
}
|
|
guard let activationOutcome = activateViaPrivateWindowServerAPI(window),
|
activationOutcome.succeeded
|
else {
|
DevelopmentDiagnostics.log("windowClose.commandW.activationFailed", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"reason": reason
|
])
|
return nil
|
}
|
|
Thread.sleep(forTimeInterval: 0.08)
|
guard postCommandW(to: processIdentifier) else {
|
DevelopmentDiagnostics.log("windowClose.commandW.postFailed", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"reason": reason
|
])
|
return .failed("commandWPostFailed")
|
}
|
|
DevelopmentDiagnostics.log("windowClose.commandW.requested", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"reason": reason
|
])
|
return .requested
|
}
|
|
private func postCommandW(to processIdentifier: Int32) -> Bool {
|
let source = CGEventSource(stateID: .combinedSessionState)
|
guard let keyDown = CGEvent(
|
keyboardEventSource: source,
|
virtualKey: Self.commandWVirtualKeyCode,
|
keyDown: true
|
),
|
let keyUp = CGEvent(
|
keyboardEventSource: source,
|
virtualKey: Self.commandWVirtualKeyCode,
|
keyDown: false
|
)
|
else {
|
return false
|
}
|
|
keyDown.flags = .maskCommand
|
keyUp.flags = .maskCommand
|
keyDown.postToPid(processIdentifier)
|
keyUp.postToPid(processIdentifier)
|
return true
|
}
|
|
private func axWindow(
|
for window: AlignerWindow,
|
in axMetadata: [AXWindowMetadata],
|
operation: String = "windowClose",
|
matchAttempt: String = "initial"
|
) -> AXWindowMetadata? {
|
if let directMatch = axMetadata.first(where: { $0.windowID == window.id }) {
|
DevelopmentDiagnostics.log("\(operation).axMatch.directWindowID", [
|
"windowID": window.id,
|
"pid": window.app.processIdentifier,
|
"attempt": matchAttempt
|
])
|
return directMatch
|
}
|
|
guard let processIdentifier = window.app.processIdentifier else {
|
return nil
|
}
|
|
if let geometryFingerprint = axWindowGeometryFingerprint(for: window) {
|
let geometryMatches = axMetadata.filter { metadata in
|
AXWindowGeometryFingerprint(metadata) == geometryFingerprint
|
}
|
if geometryMatches.count == 1 {
|
DevelopmentDiagnostics.log("\(operation).axMatch.geometryFingerprint", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"attempt": matchAttempt
|
])
|
return geometryMatches[0]
|
}
|
|
if geometryMatches.count > 1 {
|
DevelopmentDiagnostics.log("\(operation).axMatch.geometryFingerprintAmbiguous", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"candidateCount": geometryMatches.count,
|
"attempt": matchAttempt
|
])
|
}
|
}
|
|
if let cgFingerprint = cgWindowFingerprint(for: window) {
|
let fingerprintMatches = axMetadata.filter { metadata in
|
AXWindowFingerprint(metadata) == cgFingerprint
|
}
|
if fingerprintMatches.count == 1 {
|
DevelopmentDiagnostics.log("\(operation).axMatch.fingerprint", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"attempt": matchAttempt
|
])
|
return fingerprintMatches[0]
|
}
|
|
if fingerprintMatches.count > 1 {
|
DevelopmentDiagnostics.log("\(operation).axMatch.fingerprintAmbiguous", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"candidateCount": fingerprintMatches.count,
|
"attempt": matchAttempt
|
])
|
}
|
}
|
|
let normalizedTitle = normalizedWindowTitle(window.title)
|
let titleMatches = axMetadata.filter { metadata in
|
metadata.processIdentifier == processIdentifier
|
&& normalizedWindowTitle(metadata.title ?? "") == normalizedTitle
|
}
|
if !normalizedTitle.isEmpty, titleMatches.count == 1 {
|
DevelopmentDiagnostics.log("\(operation).axMatch.normalizedTitle", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"attempt": matchAttempt
|
])
|
return titleMatches[0]
|
}
|
|
if window.app.bundleIdentifier == "com.apple.finder",
|
let finderMatch = finderFallbackAXWindow(
|
for: window,
|
processIdentifier: processIdentifier,
|
in: axMetadata
|
) {
|
DevelopmentDiagnostics.log("\(operation).axMatch.finderFallback", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"attempt": matchAttempt
|
])
|
return finderMatch
|
}
|
|
if titleMatches.count > 1 {
|
DevelopmentDiagnostics.log("\(operation).axMatch.titleAmbiguous", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"candidateCount": titleMatches.count,
|
"attempt": matchAttempt
|
])
|
}
|
|
return nil
|
}
|
|
private func finderFallbackAXWindow(
|
for window: AlignerWindow,
|
processIdentifier: Int32,
|
in axMetadata: [AXWindowMetadata]
|
) -> AXWindowMetadata? {
|
let normalizedTitle = normalizedWindowTitle(window.title)
|
guard !normalizedTitle.isEmpty else { return nil }
|
|
let finderCandidates = axMetadata.filter { metadata in
|
metadata.processIdentifier == processIdentifier
|
&& normalizedWindowTitle(metadata.title ?? "") == normalizedTitle
|
&& metadata.subrole == .standard
|
}
|
return finderCandidates.count == 1 ? finderCandidates[0] : nil
|
}
|
|
private func cgWindowFingerprint(for window: AlignerWindow) -> AXWindowFingerprint? {
|
guard let rawWindows = CGWindowListCopyWindowInfo(
|
[.optionIncludingWindow],
|
CGWindowID(window.id)
|
) as? [[String: Any]],
|
let rawWindow = rawWindows.first,
|
let ownerPID = int32Value(rawWindow[kCGWindowOwnerPID as String])
|
else {
|
return nil
|
}
|
|
return AXWindowFingerprint(
|
processIdentifier: ownerPID,
|
title: normalizedWindowTitle(title(rawWindow: rawWindow, axWindow: nil)),
|
size: bounds(rawWindow[kCGWindowBounds as String]).size
|
)
|
}
|
|
private func axWindowGeometryFingerprint(for window: AlignerWindow) -> AXWindowGeometryFingerprint? {
|
guard let processIdentifier = window.app.processIdentifier else { return nil }
|
let frame = cgWindowFrame(for: window) ?? window.frame
|
return AXWindowGeometryFingerprint(
|
processIdentifier: processIdentifier,
|
title: normalizedWindowTitle(window.title),
|
frame: frame
|
)
|
}
|
|
private func cgWindowFrame(for window: AlignerWindow) -> CGRect? {
|
guard let rawWindows = CGWindowListCopyWindowInfo(
|
[.optionIncludingWindow],
|
CGWindowID(window.id)
|
) as? [[String: Any]],
|
let rawWindow = rawWindows.first
|
else {
|
return nil
|
}
|
|
return bounds(rawWindow[kCGWindowBounds as String])
|
}
|
|
private func matchingAXTitleCandidateCount(
|
for window: AlignerWindow,
|
in axMetadata: [AXWindowMetadata]
|
) -> Int {
|
guard let processIdentifier = window.app.processIdentifier else { return 0 }
|
let normalizedTitle = normalizedWindowTitle(window.title)
|
return axMetadata.filter { metadata in
|
metadata.processIdentifier == processIdentifier
|
&& normalizedWindowTitle(metadata.title ?? "") == normalizedTitle
|
}.count
|
}
|
|
private func matchingAXSizeCandidateCount(
|
for window: AlignerWindow,
|
in axMetadata: [AXWindowMetadata]
|
) -> Int {
|
guard let processIdentifier = window.app.processIdentifier,
|
let cgFingerprint = cgWindowFingerprint(for: window)
|
else {
|
return 0
|
}
|
|
return axMetadata.filter { metadata in
|
metadata.processIdentifier == processIdentifier
|
&& Int(metadata.size.width.rounded()) == cgFingerprint.width
|
&& Int(metadata.size.height.rounded()) == cgFingerprint.height
|
}.count
|
}
|
|
func close(app: AlignerApp) throws -> WindowCloseResult {
|
DevelopmentDiagnostics.log("windowClose.app.start", [
|
"appName": app.name,
|
"bundleIdentifier": app.bundleIdentifier,
|
"pid": app.processIdentifier
|
])
|
|
guard let processIdentifier = app.processIdentifier else {
|
DevelopmentDiagnostics.log("windowClose.app.missingPID", [
|
"appName": app.name,
|
"bundleIdentifier": app.bundleIdentifier
|
])
|
return .appNotFound
|
}
|
guard let runningApplication = NSRunningApplication(processIdentifier: processIdentifier) else {
|
DevelopmentDiagnostics.log("windowClose.app.missingApplication", [
|
"appName": app.name,
|
"pid": processIdentifier
|
])
|
return .appNotFound
|
}
|
|
let requested = runningApplication.terminate()
|
DevelopmentDiagnostics.log("windowClose.app.result", [
|
"appName": app.name,
|
"pid": processIdentifier,
|
"requested": requested
|
])
|
return requested ? .requested : .failed("terminateFailed")
|
}
|
|
private func activateViaPrivateWindowServerAPI(_ window: AlignerWindow) -> PrivateWindowActivationOutcome? {
|
guard window.identifierSource == .cgWindow,
|
let processIdentifier = window.app.processIdentifier,
|
let privateActivationBridge
|
else {
|
DevelopmentDiagnostics.log("windowActivation.privateActivation.skipped", [
|
"windowID": window.id,
|
"identifierSource": String(describing: window.identifierSource),
|
"hasPID": window.app.processIdentifier != nil,
|
"bridgeAvailable": privateActivationBridge != nil
|
])
|
return nil
|
}
|
|
let outcome = privateActivationBridge.activate(
|
processIdentifier: processIdentifier,
|
windowID: window.id
|
)
|
DevelopmentDiagnostics.log("windowActivation.privateActivation.result", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"getProcessStatus": outcome.getProcessStatus,
|
"setFrontError": outcome.setFrontError.map(String.init(describing:)),
|
"makeKeyFirstError": outcome.makeKeyFirstError.map(String.init(describing:)),
|
"makeKeySecondError": outcome.makeKeySecondError.map(String.init(describing:)),
|
"succeeded": outcome.succeeded
|
])
|
return outcome
|
}
|
|
private func focusTargetSpaceIfNeeded(for window: AlignerWindow) -> PrivateSpaceFocusOutcome? {
|
guard window.identifierSource == .cgWindow,
|
!window.spaceIDs.isEmpty,
|
let privateSpaceActivationBridge
|
else {
|
DevelopmentDiagnostics.log("windowActivation.spaceFocus.skipped", [
|
"windowID": window.id,
|
"identifierSource": String(describing: window.identifierSource),
|
"spaceIDCount": window.spaceIDs.count,
|
"bridgeAvailable": privateSpaceActivationBridge != nil
|
])
|
return nil
|
}
|
|
let outcome = privateSpaceActivationBridge.focus(spaceIDs: window.spaceIDs)
|
DevelopmentDiagnostics.log("windowActivation.spaceFocus.result", [
|
"windowID": window.id,
|
"targetSpaceID": outcome?.targetSpaceID,
|
"displayIdentifier": outcome?.displayIdentifier,
|
"previousCurrentSpaceID": outcome?.previousCurrentSpaceID,
|
"didRequestFocus": outcome?.didRequestFocus,
|
"error": outcome?.error
|
])
|
return outcome
|
}
|
|
private func windowRecords() throws -> [WindowEnumerationRecord] {
|
guard let rawWindows = CGWindowListCopyWindowInfo(
|
[.optionAll, .excludeDesktopElements],
|
kCGNullWindowID
|
) as? [[String: Any]] else {
|
return []
|
}
|
|
let axWindows = AXWindowMetadataReader.metadata(appCategorizer: appCategorizer)
|
var axMetadata: [UInt32: AXWindowMetadata] = [:]
|
for metadata in axWindows {
|
if let windowID = metadata.windowID {
|
axMetadata[windowID] = metadata
|
}
|
}
|
let axBackedTitleKeys = Set(axMetadata.values.compactMap(AXWindowTitleKey.init))
|
let cgWindowIDs = Set(rawWindows.compactMap { uint32Value($0[kCGWindowNumber as String]) })
|
var fallbackAXMetadataByFingerprint: [AXWindowFingerprint: [AXWindowMetadata]] = [:]
|
for metadata in axWindows {
|
if let windowID = metadata.windowID, cgWindowIDs.contains(windowID) {
|
continue
|
}
|
|
fallbackAXMetadataByFingerprint[AXWindowFingerprint(metadata), default: []].append(metadata)
|
}
|
let windowIDs = Array(cgWindowIDs.union(axMetadata.keys))
|
let spaceIDsByWindowID = spaceIDsByWindowIDProvider(windowIDs)
|
let cgWindowFingerprints = Set(rawWindows.compactMap { rawWindow -> AXWindowFingerprint? in
|
guard let ownerPID = int32Value(rawWindow[kCGWindowOwnerPID as String]) else {
|
return nil
|
}
|
|
let windowID = uint32Value(rawWindow[kCGWindowNumber as String])
|
let axWindow = windowID.flatMap { axMetadata[$0] }
|
return AXWindowFingerprint(
|
processIdentifier: ownerPID,
|
title: title(rawWindow: rawWindow, axWindow: axWindow),
|
size: bounds(rawWindow[kCGWindowBounds as String]).size
|
)
|
})
|
var occupiedWindowIDs = Set(windowIDs)
|
var nextRestorableAXWindowsByID: [UInt32: RestorableAXWindow] = [:]
|
var consumedAXMetadata = Set<AXWindowIdentity>()
|
|
let cgRecords: [WindowEnumerationRecord] = rawWindows.compactMap { rawWindow in
|
guard let windowID = uint32Value(rawWindow[kCGWindowNumber as String]) else {
|
return nil
|
}
|
|
let rawBounds = bounds(rawWindow[kCGWindowBounds as String])
|
let ownerPID = int32Value(rawWindow[kCGWindowOwnerPID as String])
|
let runningApplication = ownerPID.flatMap(NSRunningApplication.init(processIdentifier:))
|
let rawFingerprint = ownerPID.map {
|
AXWindowFingerprint(
|
processIdentifier: $0,
|
title: title(rawWindow: rawWindow, axWindow: nil),
|
size: rawBounds.size
|
)
|
}
|
let directAXWindow = axMetadata[windowID]
|
let fallbackAXWindow = rawFingerprint.flatMap { fingerprint in
|
fallbackAXMetadataByFingerprint[fingerprint]?.first { metadata in
|
!consumedAXMetadata.contains(AXWindowIdentity(metadata))
|
}
|
}
|
let axWindow = directAXWindow ?? fallbackAXWindow
|
let spaceIDs = spaceIDsByWindowID[windowID] ?? []
|
let hasAXBackedTitleDuplicate = axWindow == nil
|
&& ownerPID.flatMap { AXWindowTitleKey(processIdentifier: $0, title: title(rawWindow: rawWindow, axWindow: nil)) }
|
.map { axBackedTitleKeys.contains($0) } == true
|
let app = app(from: runningApplication, rawWindow: rawWindow, ownerPID: ownerPID)
|
let record = WindowEnumerationRecord(
|
id: windowID,
|
app: app,
|
activationPolicy: activationPolicy(from: runningApplication),
|
title: title(rawWindow: rawWindow, axWindow: axWindow),
|
size: rawBounds.size,
|
frame: rawBounds,
|
subrole: axWindow?.subrole ?? .standard,
|
isMinimized: axWindow?.isMinimized ?? false,
|
isFullscreen: !Set(spaceIDs).isDisjoint(with: fullscreenSpaceIDs),
|
isGhost: isGhost(
|
rawWindow: rawWindow,
|
axWindow: axWindow,
|
app: app,
|
spaceIDs: spaceIDs,
|
hasAXBackedTitleDuplicate: hasAXBackedTitleDuplicate
|
),
|
isDesktopElement: isDesktopElement(rawWindow: rawWindow, app: app),
|
isSystemCritical: isSystemCritical(rawWindow: rawWindow, app: app),
|
isInteractable: axWindow?.isInteractable ?? true,
|
hasAXBacking: axWindow != nil,
|
isOnscreen: boolValue(rawWindow[kCGWindowIsOnscreen as String]),
|
spaceIDs: spaceIDs
|
)
|
|
if axWindow?.isMinimized == true, let axWindow {
|
nextRestorableAXWindowsByID[windowID] = RestorableAXWindow(
|
processIdentifier: axWindow.processIdentifier,
|
element: axWindow.element
|
)
|
}
|
if let axWindow, !record.isGhost {
|
consumedAXMetadata.insert(AXWindowIdentity(axWindow))
|
}
|
|
return record
|
}
|
|
var axOnlyRecords: [WindowEnumerationRecord] = []
|
for axWindow in axMetadata.values where axWindow.isMinimized {
|
guard let windowID = axWindow.windowID else { continue }
|
nextRestorableAXWindowsByID[windowID] = RestorableAXWindow(
|
processIdentifier: axWindow.processIdentifier,
|
element: axWindow.element
|
)
|
}
|
|
for axWindow in axWindows {
|
guard !consumedAXMetadata.contains(AXWindowIdentity(axWindow)) else { continue }
|
guard axWindow.subrole.isExplicitlySupported else { continue }
|
guard axWindow.hasExplicitSubrole else { continue }
|
|
let resolvedWindowID: UInt32
|
let identifierSource: AlignerWindow.IdentifierSource
|
if let windowID = axWindow.windowID {
|
guard !cgWindowIDs.contains(windowID) else { continue }
|
resolvedWindowID = windowID
|
identifierSource = .cgWindow
|
} else if shouldCreateSyntheticAXFallback(for: axWindow) {
|
guard !cgWindowFingerprints.contains(AXWindowFingerprint(axWindow)) else { continue }
|
|
resolvedWindowID = MinimizedWindowFallbackPolicy.syntheticWindowID(
|
processIdentifier: axWindow.processIdentifier,
|
windowIndex: axWindow.windowIndex,
|
title: axWindow.title ?? "",
|
subroleDescription: axWindow.subrole.fallbackDescription,
|
occupiedWindowIDs: occupiedWindowIDs
|
)
|
occupiedWindowIDs.insert(resolvedWindowID)
|
identifierSource = .syntheticAX
|
} else {
|
continue
|
}
|
|
let spaceIDs = spaceIDsByWindowID[resolvedWindowID] ?? []
|
nextRestorableAXWindowsByID[resolvedWindowID] = RestorableAXWindow(
|
processIdentifier: axWindow.processIdentifier,
|
element: axWindow.element
|
)
|
axOnlyRecords.append(WindowEnumerationRecord(
|
id: resolvedWindowID,
|
app: axWindow.app,
|
activationPolicy: axWindow.activationPolicy,
|
title: axWindow.title ?? "",
|
identifierSource: identifierSource,
|
size: axWindow.size,
|
frame: axWindow.frame,
|
subrole: axWindow.subrole,
|
isMinimized: axWindow.isMinimized,
|
isFullscreen: !Set(spaceIDs).isDisjoint(with: fullscreenSpaceIDs),
|
isGhost: false,
|
isDesktopElement: false,
|
isSystemCritical: isSystemCritical(app: axWindow.app),
|
isInteractable: axWindow.isInteractable,
|
hasAXBacking: true,
|
isOnscreen: false,
|
spaceIDs: spaceIDs
|
))
|
}
|
|
restorableAXWindowsByID = nextRestorableAXWindowsByID
|
return cgRecords + axOnlyRecords
|
}
|
|
private func app(
|
from runningApplication: NSRunningApplication?,
|
rawWindow: [String: Any],
|
ownerPID: Int32?
|
) -> AlignerApp {
|
let bundleIdentifier = runningApplication?.bundleIdentifier ?? ""
|
let name = runningApplication?.localizedName
|
?? stringValue(rawWindow[kCGWindowOwnerName as String])
|
?? "Unknown"
|
let provisionalApp = AlignerApp(
|
bundleIdentifier: bundleIdentifier,
|
name: name,
|
category: .generic,
|
isHidden: runningApplication?.isHidden ?? false,
|
executablePath: runningApplication?.bundleURL?.path,
|
processIdentifier: ownerPID
|
)
|
|
return AlignerApp(
|
bundleIdentifier: provisionalApp.bundleIdentifier,
|
name: provisionalApp.name,
|
category: appCategorizer.category(for: provisionalApp),
|
isHidden: provisionalApp.isHidden,
|
executablePath: provisionalApp.executablePath,
|
processIdentifier: provisionalApp.processIdentifier
|
)
|
}
|
|
private func activateApplication(for window: AlignerWindow) -> Bool {
|
guard
|
let processIdentifier = window.app.processIdentifier
|
else {
|
return false
|
}
|
|
return activateApplication(processIdentifier: processIdentifier)
|
}
|
|
private func activateApplication(processIdentifier: Int32) -> Bool {
|
guard let application = NSRunningApplication(processIdentifier: processIdentifier) else {
|
DevelopmentDiagnostics.log("windowActivation.activateApplication.missingApplication", [
|
"pid": processIdentifier
|
])
|
return false
|
}
|
|
let activated = application.activate(options: [.activateAllWindows])
|
DevelopmentDiagnostics.log("windowActivation.activateApplication", [
|
"pid": processIdentifier,
|
"localizedName": application.localizedName,
|
"bundleIdentifier": application.bundleIdentifier,
|
"activationPolicy": application.activationPolicy.rawValue,
|
"activateOptions": "activateAllWindows",
|
"isActive": application.isActive,
|
"result": activated
|
])
|
return activated
|
}
|
|
private func focusWindowResult(_ window: AXUIElement, processIdentifier: Int32) -> AXActionResult {
|
let appElement = AXUIElementCreateApplication(processIdentifier)
|
let error = AXUIElementSetAttributeValue(
|
appElement,
|
kAXFocusedWindowAttribute as CFString,
|
window
|
)
|
return AXActionResult(error: error)
|
}
|
|
private func raiseWindowResult(_ window: AXUIElement) -> AXActionResult {
|
AXActionResult(error: AXUIElementPerformAction(window, kAXRaiseAction as CFString))
|
}
|
|
private func activationPolicy(from runningApplication: NSRunningApplication?) -> AlignerActivationPolicy {
|
guard let runningApplication else { return .unknown }
|
|
switch runningApplication.activationPolicy {
|
case .regular:
|
return .regular
|
case .accessory:
|
return .accessory
|
case .prohibited:
|
return .prohibited
|
@unknown default:
|
return .unknown
|
}
|
}
|
|
private func title(rawWindow: [String: Any], axWindow: AXWindowMetadata?) -> String {
|
if let axTitle = axWindow?.title, !axTitle.isEmpty {
|
return axTitle
|
}
|
|
return stringValue(rawWindow[kCGWindowName as String]) ?? ""
|
}
|
|
private func isGhost(
|
rawWindow: [String: Any],
|
axWindow: AXWindowMetadata?,
|
app: AlignerApp,
|
spaceIDs: [UInt64],
|
hasAXBackedTitleDuplicate: Bool = false
|
) -> Bool {
|
let layer = intValue(rawWindow[kCGWindowLayer as String]) ?? 0
|
let alpha = doubleValue(rawWindow[kCGWindowAlpha as String]) ?? 1
|
let isOnscreen = boolValue(rawWindow[kCGWindowIsOnscreen as String])
|
let rawTitle = stringValue(rawWindow[kCGWindowName as String]) ?? ""
|
let rawBounds = bounds(rawWindow[kCGWindowBounds as String])
|
|
if axWindow?.isMinimized == true {
|
return false
|
}
|
if axWindow != nil {
|
if alpha <= 0 {
|
return true
|
}
|
if layer == 0 {
|
return false
|
}
|
|
return !isOnscreen
|
}
|
if hasAXBackedTitleDuplicate {
|
return true
|
}
|
if isKnownCGOnlyMainWindow(rawWindow: rawWindow, app: app) {
|
return false
|
}
|
if !spaceIDs.isEmpty, isLikelyFullscreenAuxiliaryCGWindow(bounds: rawBounds) {
|
return true
|
}
|
if rawTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, alpha < 0.99 {
|
return true
|
}
|
if !spaceIDs.isEmpty {
|
return layer != 0 || alpha <= 0
|
}
|
|
return layer != 0 || alpha <= 0 || !isOnscreen
|
}
|
|
private func isLikelyFullscreenAuxiliaryCGWindow(bounds: CGRect) -> Bool {
|
bounds.width < 500 || bounds.height < 160
|
}
|
|
private func isKnownCGOnlyMainWindow(rawWindow: [String: Any], app: AlignerApp) -> Bool {
|
let layer = intValue(rawWindow[kCGWindowLayer as String]) ?? 0
|
let alpha = doubleValue(rawWindow[kCGWindowAlpha as String]) ?? 1
|
let isOnscreen = boolValue(rawWindow[kCGWindowIsOnscreen as String])
|
let title = stringValue(rawWindow[kCGWindowName as String]) ?? ""
|
let rawBounds = bounds(rawWindow[kCGWindowBounds as String])
|
let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if app.bundleIdentifier == "cc.ffitch.shottr" {
|
return isOnscreen
|
&& alpha > 0
|
&& trimmedTitle == "Shottr"
|
&& rawBounds.width > WindowEnumerationPolicy.minimumVisibleWidth
|
&& rawBounds.height > WindowEnumerationPolicy.minimumVisibleHeight
|
}
|
|
if app.bundleIdentifier == "com.apple.finder" {
|
return layer == 0
|
&& alpha > 0
|
&& !trimmedTitle.isEmpty
|
&& rawBounds.width > WindowEnumerationPolicy.minimumVisibleWidth
|
&& rawBounds.height > WindowEnumerationPolicy.minimumVisibleHeight
|
}
|
|
guard app.bundleIdentifier == "io.dcloud.HBuilderX" else { return false }
|
|
return layer == 0
|
&& alpha > 0
|
&& !isOnscreen
|
&& !trimmedTitle.isEmpty
|
&& rawBounds.width > WindowEnumerationPolicy.minimumVisibleWidth
|
&& rawBounds.height > WindowEnumerationPolicy.minimumVisibleHeight
|
}
|
|
private func isDesktopElement(rawWindow: [String: Any], app: AlignerApp) -> Bool {
|
guard app.bundleIdentifier == "com.apple.finder" else { return false }
|
let title = stringValue(rawWindow[kCGWindowName as String]) ?? ""
|
let ownerName = stringValue(rawWindow[kCGWindowOwnerName as String]) ?? ""
|
return title == "Desktop" || ownerName == "Dock"
|
}
|
|
private func isSystemCritical(rawWindow: [String: Any], app: AlignerApp) -> Bool {
|
let title = stringValue(rawWindow[kCGWindowName as String]) ?? ""
|
return SystemCriticalWindowClassifier.isSystemCritical(app: app)
|
|| SystemCriticalWindowClassifier.isSystemCriticalTitle(title)
|
}
|
|
private func isSystemCritical(app: AlignerApp) -> Bool {
|
SystemCriticalWindowClassifier.isSystemCritical(app: app)
|
}
|
|
private func shouldCreateSyntheticAXFallback(for axWindow: AXWindowMetadata) -> Bool {
|
MinimizedWindowFallbackPolicy.shouldCreateAXOnlyFallback(
|
hasCGWindowID: axWindow.windowID != nil,
|
isMinimized: axWindow.isMinimized
|
)
|
}
|
|
private static func restoreResultString(_ result: WindowRestoreResult) -> String {
|
switch result {
|
case .restored:
|
return "restored"
|
case .windowNotFound:
|
return "windowNotFound"
|
case .unsupported:
|
return "unsupported"
|
case .activationFailed:
|
return "activationFailed"
|
}
|
}
|
|
private static let commandWVirtualKeyCode: CGKeyCode = 13
|
}
|
|
private struct AXWindowMetadata {
|
let windowID: UInt32?
|
let app: AlignerApp
|
let activationPolicy: AlignerActivationPolicy
|
let title: String?
|
let isMinimized: Bool
|
let size: CGSize
|
let frame: CGRect?
|
let subrole: AlignerWindowSubrole
|
let hasExplicitSubrole: Bool
|
let isInteractable: Bool
|
let processIdentifier: Int32
|
let windowIndex: Int
|
let element: AXUIElement
|
}
|
|
private struct AXWindowIdentity: Hashable {
|
let processIdentifier: Int32
|
let windowID: UInt32?
|
let windowIndex: Int
|
|
init(_ metadata: AXWindowMetadata) {
|
processIdentifier = metadata.processIdentifier
|
windowID = metadata.windowID
|
windowIndex = metadata.windowIndex
|
}
|
}
|
|
private struct AXWindowTitleKey: Hashable {
|
let processIdentifier: Int32
|
let title: String
|
|
init?(processIdentifier: Int32, title: String?) {
|
let normalizedTitle = title?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
guard !normalizedTitle.isEmpty else { return nil }
|
|
self.processIdentifier = processIdentifier
|
self.title = normalizedTitle
|
}
|
|
init?(_ metadata: AXWindowMetadata) {
|
self.init(processIdentifier: metadata.processIdentifier, title: metadata.title)
|
}
|
}
|
|
private struct AXWindowFingerprint: Hashable {
|
let processIdentifier: Int32
|
let title: String
|
let width: Int
|
let height: Int
|
|
init(processIdentifier: Int32, title: String, size: CGSize) {
|
self.processIdentifier = processIdentifier
|
self.title = title
|
self.width = Int(size.width.rounded())
|
self.height = Int(size.height.rounded())
|
}
|
|
init(_ metadata: AXWindowMetadata) {
|
self.init(
|
processIdentifier: metadata.processIdentifier,
|
title: metadata.title ?? "",
|
size: metadata.size
|
)
|
}
|
}
|
|
private struct AXWindowGeometryFingerprint: Hashable {
|
let processIdentifier: Int32
|
let title: String
|
let x: Int
|
let y: Int
|
let width: Int
|
let height: Int
|
|
init?(processIdentifier: Int32, title: String, frame: CGRect?) {
|
guard let frame else { return nil }
|
|
self.processIdentifier = processIdentifier
|
self.title = title
|
self.x = Int(frame.origin.x.rounded())
|
self.y = Int(frame.origin.y.rounded())
|
self.width = Int(frame.size.width.rounded())
|
self.height = Int(frame.size.height.rounded())
|
}
|
|
init?(_ metadata: AXWindowMetadata) {
|
self.init(
|
processIdentifier: metadata.processIdentifier,
|
title: normalizedWindowTitle(metadata.title ?? ""),
|
frame: metadata.frame
|
)
|
}
|
}
|
|
private struct RestorableAXWindow {
|
let processIdentifier: Int32
|
let element: AXUIElement
|
}
|
|
private struct AXActionResult {
|
let error: AXError
|
|
var success: Bool {
|
error == .success
|
}
|
}
|
|
private enum AXWindowRestoreError: Error {
|
case setMinimizedFailed(AXError)
|
case raiseFailed(AXError)
|
}
|
|
private enum AXWindowMetadataReader {
|
static func metadata(appCategorizer: any AppCategorizer) -> [AXWindowMetadata] {
|
var result: [AXWindowMetadata] = []
|
|
for application in NSWorkspace.shared.runningApplications {
|
guard application.activationPolicy == .regular else { continue }
|
let app = app(from: application, appCategorizer: appCategorizer)
|
|
let appElement = AXUIElementCreateApplication(application.processIdentifier)
|
let windows = windows(for: appElement)
|
guard !windows.isEmpty else { continue }
|
|
for (windowIndex, window) in windows.enumerated() {
|
let subrole = subrole(for: window)
|
let size = size(for: window)
|
let position = position(for: window)
|
result.append(AXWindowMetadata(
|
windowID: windowID(for: window),
|
app: app,
|
activationPolicy: activationPolicy(from: application),
|
title: title(for: window),
|
isMinimized: boolAttribute(kAXMinimizedAttribute as String, for: window) ?? false,
|
size: size,
|
frame: position.map { CGRect(origin: $0, size: size) },
|
subrole: subrole.value,
|
hasExplicitSubrole: subrole.isExplicit,
|
isInteractable: boolAttribute(kAXEnabledAttribute as String, for: window) ?? true,
|
processIdentifier: application.processIdentifier,
|
windowIndex: windowIndex,
|
element: window
|
))
|
}
|
}
|
|
return result
|
}
|
|
private static func windows(for appElement: AXUIElement) -> [AXUIElement] {
|
var result: [AXUIElement] = []
|
var seen = Set<AXWindowElementKey>()
|
|
func append(_ window: AXUIElement) {
|
let key = AXWindowElementKey(window)
|
guard !seen.contains(key) else { return }
|
seen.insert(key)
|
result.append(window)
|
}
|
|
var windowsValue: CFTypeRef?
|
if AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &windowsValue) == .success,
|
let windows = windowsValue as? [AXUIElement] {
|
windows.forEach(append)
|
}
|
|
for attribute in [kAXFocusedWindowAttribute, kAXMainWindowAttribute] {
|
var windowValue: CFTypeRef?
|
guard AXUIElementCopyAttributeValue(appElement, attribute as CFString, &windowValue) == .success,
|
let window = windowValue,
|
CFGetTypeID(window) == AXUIElementGetTypeID()
|
else {
|
continue
|
}
|
|
append(window as! AXUIElement)
|
}
|
|
return result
|
}
|
|
private static func app(
|
from application: NSRunningApplication,
|
appCategorizer: any AppCategorizer
|
) -> AlignerApp {
|
let provisionalApp = AlignerApp(
|
bundleIdentifier: application.bundleIdentifier ?? "",
|
name: application.localizedName ?? "Unknown",
|
category: .generic,
|
isHidden: application.isHidden,
|
executablePath: application.bundleURL?.path,
|
processIdentifier: application.processIdentifier
|
)
|
|
return AlignerApp(
|
bundleIdentifier: provisionalApp.bundleIdentifier,
|
name: provisionalApp.name,
|
category: appCategorizer.category(for: provisionalApp),
|
isHidden: provisionalApp.isHidden,
|
executablePath: provisionalApp.executablePath,
|
processIdentifier: provisionalApp.processIdentifier
|
)
|
}
|
|
private static func activationPolicy(from application: NSRunningApplication) -> AlignerActivationPolicy {
|
switch application.activationPolicy {
|
case .regular:
|
return .regular
|
case .accessory:
|
return .accessory
|
case .prohibited:
|
return .prohibited
|
@unknown default:
|
return .unknown
|
}
|
}
|
|
private static func windowID(for window: AXUIElement) -> UInt32? {
|
var value: CFTypeRef?
|
if AXUIElementCopyAttributeValue(window, "AXWindowNumber" as CFString, &value) == .success,
|
let windowID = uint32Value(value) {
|
return windowID
|
}
|
|
return PrivateAXWindowIDReader.windowID(for: window)
|
}
|
|
private static func title(for window: AXUIElement) -> String? {
|
var value: CFTypeRef?
|
guard AXUIElementCopyAttributeValue(window, kAXTitleAttribute as CFString, &value) == .success else {
|
return nil
|
}
|
|
return value as? String
|
}
|
|
private static func boolAttribute(_ attribute: String, for window: AXUIElement) -> Bool? {
|
var value: CFTypeRef?
|
guard AXUIElementCopyAttributeValue(window, attribute as CFString, &value) == .success else {
|
return nil
|
}
|
|
return boolValue(value)
|
}
|
|
private static func size(for window: AXUIElement) -> CGSize {
|
var value: CFTypeRef?
|
guard AXUIElementCopyAttributeValue(window, kAXSizeAttribute as CFString, &value) == .success,
|
let axValue = value,
|
CFGetTypeID(axValue) == AXValueGetTypeID()
|
else {
|
return .zero
|
}
|
|
var size = CGSize.zero
|
guard AXValueGetValue(axValue as! AXValue, .cgSize, &size) else {
|
return .zero
|
}
|
|
return size
|
}
|
|
private static func position(for window: AXUIElement) -> CGPoint? {
|
var value: CFTypeRef?
|
guard AXUIElementCopyAttributeValue(window, kAXPositionAttribute as CFString, &value) == .success,
|
let axValue = value,
|
CFGetTypeID(axValue) == AXValueGetTypeID()
|
else {
|
return nil
|
}
|
|
var position = CGPoint.zero
|
guard AXValueGetValue(axValue as! AXValue, .cgPoint, &position) else {
|
return nil
|
}
|
|
return position
|
}
|
|
private static func subrole(for window: AXUIElement) -> (value: AlignerWindowSubrole, isExplicit: Bool) {
|
var value: CFTypeRef?
|
guard AXUIElementCopyAttributeValue(window, kAXSubroleAttribute as CFString, &value) == .success,
|
let subrole = value as? String
|
else {
|
return (.standard, false)
|
}
|
|
switch subrole {
|
case "AXStandardWindow":
|
return (.standard, true)
|
case "AXDialog", "AXModalDialog":
|
return (.dialog, true)
|
case "AXSheet":
|
return (.sheet, true)
|
default:
|
return (.unknown(subrole), true)
|
}
|
}
|
}
|
|
private struct AXWindowElementKey: Hashable {
|
private let typeID: CFTypeID
|
private let pointer: UInt
|
private let windowID: UInt32?
|
|
init(_ element: AXUIElement) {
|
self.typeID = CFGetTypeID(element)
|
self.pointer = UInt(bitPattern: Unmanaged.passUnretained(element).toOpaque())
|
self.windowID = PrivateAXWindowIDReader.windowID(for: element)
|
}
|
|
static func == (lhs: AXWindowElementKey, rhs: AXWindowElementKey) -> Bool {
|
if let lhsWindowID = lhs.windowID,
|
let rhsWindowID = rhs.windowID {
|
return lhsWindowID == rhsWindowID
|
}
|
|
return lhs.typeID == rhs.typeID && lhs.pointer == rhs.pointer
|
}
|
|
func hash(into hasher: inout Hasher) {
|
if let windowID {
|
hasher.combine("windowID")
|
hasher.combine(windowID)
|
return
|
}
|
|
hasher.combine("pointer")
|
hasher.combine(typeID)
|
hasher.combine(pointer)
|
}
|
}
|
|
private extension AlignerWindowSubrole {
|
var isExplicitlySupported: Bool {
|
switch self {
|
case .standard, .dialog, .sheet:
|
return true
|
case .unknown:
|
return false
|
}
|
}
|
|
var fallbackDescription: String {
|
switch self {
|
case .standard:
|
return "standard"
|
case .dialog:
|
return "dialog"
|
case .sheet:
|
return "sheet"
|
case .unknown(let rawValue):
|
return rawValue
|
}
|
}
|
}
|
|
private func bounds(_ value: Any?) -> CGRect {
|
guard let dictionary = value as? NSDictionary else {
|
return .zero
|
}
|
|
var rect = CGRect.zero
|
CGRectMakeWithDictionaryRepresentation(dictionary as CFDictionary, &rect)
|
return rect
|
}
|
|
private func stringValue(_ value: Any?) -> String? {
|
value as? String
|
}
|
|
private func normalizedWindowTitle(_ title: String) -> String {
|
title.trimmingCharacters(in: .whitespacesAndNewlines)
|
}
|
|
private func boolValue(_ value: Any?) -> Bool {
|
switch value {
|
case let bool as Bool:
|
return bool
|
case let number as NSNumber:
|
return number.boolValue
|
default:
|
return false
|
}
|
}
|
|
private func intValue(_ value: Any?) -> Int? {
|
switch value {
|
case let number as NSNumber:
|
return number.intValue
|
case let int as Int:
|
return int
|
default:
|
return nil
|
}
|
}
|
|
private func int32Value(_ value: Any?) -> Int32? {
|
switch value {
|
case let number as NSNumber:
|
return number.int32Value
|
case let int32 as Int32:
|
return int32
|
case let int as Int:
|
return Int32(int)
|
default:
|
return nil
|
}
|
}
|
|
private func uint32Value(_ value: Any?) -> UInt32? {
|
switch value {
|
case let number as NSNumber:
|
return number.uint32Value
|
case let uint32 as UInt32:
|
return uint32
|
case let int as Int where int >= 0:
|
return UInt32(int)
|
default:
|
return nil
|
}
|
}
|
|
private func doubleValue(_ value: Any?) -> Double? {
|
switch value {
|
case let number as NSNumber:
|
return number.doubleValue
|
case let double as Double:
|
return double
|
default:
|
return nil
|
}
|
}
|