From a4af5d51e8fbb8a0e53f98aa9f9e238536aaba24 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 12 Jun 2026 21:08:39 +0800
Subject: [PATCH] Render split view spaces with ordered app halves

---
 C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift |  440 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 428 insertions(+), 12 deletions(-)

diff --git a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
index 773abed..55b5f44 100644
--- a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
+++ b/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,13 +669,15 @@
                 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),
                 isGhost: isGhost(
                     rawWindow: rawWindow,
                     axWindow: axWindow,
+                    app: app,
                     spaceIDs: spaceIDs,
                     hasAXBackedTitleDuplicate: hasAXBackedTitleDuplicate
                 ),
@@ -467,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),
@@ -583,6 +867,7 @@
     private func isGhost(
         rawWindow: [String: Any],
         axWindow: AXWindowMetadata?,
+        app: AlignerApp,
         spaceIDs: [UInt64],
         hasAXBackedTitleDuplicate: Bool = false
     ) -> Bool {
@@ -596,10 +881,20 @@
             return false
         }
         if axWindow != nil {
-            return layer != 0 || alpha <= 0
+            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
@@ -616,6 +911,40 @@
 
     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 {
@@ -654,6 +983,7 @@
             return "activationFailed"
         }
     }
+
 }
 
 private struct AXWindowMetadata {
@@ -663,6 +993,7 @@
     let title: String?
     let isMinimized: Bool
     let size: CGSize
+    let frame: CGRect?
     let subrole: AlignerWindowSubrole
     let hasExplicitSubrole: Bool
     let isInteractable: Bool
@@ -749,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,
@@ -773,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
@@ -859,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,
@@ -877,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)
     }
 }
 
@@ -918,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:

--
Gitblit v1.9.3