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 | 395 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 383 insertions(+), 12 deletions(-)
diff --git a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
index 799c87d..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,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,7 +922,8 @@
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
@@ -699,6 +983,7 @@
return "activationFailed"
}
}
+
}
private struct AXWindowMetadata {
@@ -708,6 +993,7 @@
let title: String?
let isMinimized: Bool
let size: CGSize
+ let frame: CGRect?
let subrole: AlignerWindowSubrole
let hasExplicitSubrole: Bool
let isInteractable: Bool
@@ -794,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,
@@ -818,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
@@ -904,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,
@@ -922,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)
}
}
@@ -963,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