Ariver
2026-06-12 a4af5d51e8fbb8a0e53f98aa9f9e238536aaba24
C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
@@ -1,10 +1,10 @@
import AppKit
import ApplicationServices
@preconcurrency import ApplicationServices
import CoreGraphics
import Foundation
import AlignerCore
final class CGWindowAXWindowService: WindowServiceProtocol, WindowRestorationServiceProtocol, WindowActivationServiceProtocol {
final class CGWindowAXWindowService: WindowServiceProtocol, WindowRestorationServiceProtocol, WindowActivationServiceProtocol, WindowCloseServiceProtocol {
    weak var delegate: WindowServiceDelegate?
    private let appCategorizer: any AppCategorizer
@@ -257,6 +257,286 @@
            : .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
        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
        }
        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 axWindow(
        for window: AlignerWindow,
        in axMetadata: [AXWindowMetadata],
        matchAttempt: String = "initial"
    ) -> AXWindowMetadata? {
        if let directMatch = axMetadata.first(where: { $0.windowID == window.id }) {
            DevelopmentDiagnostics.log("windowClose.axMatch.directWindowID", [
                "windowID": window.id,
                "pid": window.app.processIdentifier,
                "attempt": matchAttempt
            ])
            return directMatch
        }
        guard let processIdentifier = window.app.processIdentifier else {
            return nil
        }
        if let cgFingerprint = cgWindowFingerprint(for: window) {
            let fingerprintMatches = axMetadata.filter { metadata in
                AXWindowFingerprint(metadata) == cgFingerprint
            }
            if fingerprintMatches.count == 1 {
                DevelopmentDiagnostics.log("windowClose.axMatch.fingerprint", [
                    "windowID": window.id,
                    "pid": processIdentifier,
                    "attempt": matchAttempt
                ])
                return fingerprintMatches[0]
            }
            if fingerprintMatches.count > 1 {
                DevelopmentDiagnostics.log("windowClose.axMatch.fingerprintAmbiguous", [
                    "windowID": window.id,
                    "pid": processIdentifier,
                    "candidateCount": fingerprintMatches.count
                ])
            }
        }
        let normalizedTitle = normalizedWindowTitle(window.title)
        let titleMatches = axMetadata.filter { metadata in
            metadata.processIdentifier == processIdentifier
                && normalizedWindowTitle(metadata.title ?? "") == normalizedTitle
        }
        if titleMatches.count == 1 {
            DevelopmentDiagnostics.log("windowClose.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("windowClose.axMatch.finderFallback", [
                "windowID": window.id,
                "pid": processIdentifier,
                "attempt": matchAttempt
            ])
            return finderMatch
        }
        if titleMatches.count > 1 {
            DevelopmentDiagnostics.log("windowClose.axMatch.titleAmbiguous", [
                "windowID": window.id,
                "pid": processIdentifier,
                "candidateCount": titleMatches.count
            ])
        }
        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 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,
@@ -362,13 +642,14 @@
                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: bounds(rawWindow[kCGWindowBounds as String]).size
                    size: rawBounds.size
                )
            }
            let directAXWindow = axMetadata[windowID]
@@ -388,7 +669,8 @@
                app: app,
                activationPolicy: activationPolicy(from: runningApplication),
                title: title(rawWindow: rawWindow, axWindow: axWindow),
                size: bounds(rawWindow[kCGWindowBounds as String]).size,
                size: rawBounds.size,
                frame: rawBounds,
                subrole: axWindow?.subrole ?? .standard,
                isMinimized: axWindow?.isMinimized ?? false,
                isFullscreen: !Set(spaceIDs).isDisjoint(with: fullscreenSpaceIDs),
@@ -468,6 +750,7 @@
                title: axWindow.title ?? "",
                identifierSource: identifierSource,
                size: axWindow.size,
                frame: axWindow.frame,
                subrole: axWindow.subrole,
                isMinimized: axWindow.isMinimized,
                isFullscreen: !Set(spaceIDs).isDisjoint(with: fullscreenSpaceIDs),
@@ -639,8 +922,17 @@
        let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
        if app.bundleIdentifier == "cc.ffitch.shottr" {
            return alpha > 0
            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
        }
@@ -691,6 +983,7 @@
            return "activationFailed"
        }
    }
}
private struct AXWindowMetadata {
@@ -700,6 +993,7 @@
    let title: String?
    let isMinimized: Bool
    let size: CGSize
    let frame: CGRect?
    let subrole: AlignerWindowSubrole
    let hasExplicitSubrole: Bool
    let isInteractable: Bool
@@ -786,22 +1080,21 @@
            let app = app(from: application, appCategorizer: appCategorizer)
            let appElement = AXUIElementCreateApplication(application.processIdentifier)
            var windowsValue: CFTypeRef?
            guard AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &windowsValue) == .success,
                  let windows = windowsValue as? [AXUIElement]
            else {
                continue
            }
            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(for: window),
                    size: size,
                    frame: position.map { CGRect(origin: $0, size: size) },
                    subrole: subrole.value,
                    hasExplicitSubrole: subrole.isExplicit,
                    isInteractable: boolAttribute(kAXEnabledAttribute as String, for: window) ?? true,
@@ -810,6 +1103,38 @@
                    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
@@ -896,6 +1221,23 @@
        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,
@@ -914,6 +1256,39 @@
        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)
    }
}
@@ -955,6 +1330,10 @@
    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: