diff --git a/Sources/GrammaGem/System/AXSupport.swift b/Sources/GrammaGem/System/AXSupport.swift index 7dd5567..a80141c 100644 --- a/Sources/GrammaGem/System/AXSupport.swift +++ b/Sources/GrammaGem/System/AXSupport.swift @@ -1,4 +1,5 @@ import Foundation +import AppKit import ApplicationServices /// Safe Accessibility helpers shared across the System layer. @@ -36,4 +37,29 @@ enum AX { static func setMessagingTimeout(_ element: AXUIElement, _ seconds: Float) { AXUIElementSetMessagingTimeout(element, seconds) } + + /// The currently-focused UI element across the system — robust to the + /// system-wide element failing. + /// + /// On recent macOS, `AXUIElementCreateSystemWide()` returns + /// `kAXErrorCannotComplete` (-25204) for *every* query made by a background / + /// accessory process (which is exactly what a menu-bar app like GrammaGem is). + /// When that happens the system-wide focused-element read yields nil and the + /// app "sees" no text or text fields at all. Resolving the focused element + /// through the **frontmost application's** own AX element works reliably in + /// that context, so we prefer the system-wide read (one IPC hop, correct on + /// OSes where it works) and transparently fall back to the per-app read. + static func focusedElement() -> AXUIElement? { + let systemWide = AXUIElementCreateSystemWide() + setMessagingTimeout(systemWide, 0.5) + if let element = copyElement(systemWide, kAXFocusedUIElementAttribute as String) { + return element + } + guard let pid = NSWorkspace.shared.frontmostApplication?.processIdentifier else { + return nil + } + let axApp = AXUIElementCreateApplication(pid) + setMessagingTimeout(axApp, 0.5) + return copyElement(axApp, kAXFocusedUIElementAttribute as String) + } } diff --git a/Sources/GrammaGem/System/TextCapture.swift b/Sources/GrammaGem/System/TextCapture.swift index 8c1a090..e9df9a2 100644 --- a/Sources/GrammaGem/System/TextCapture.swift +++ b/Sources/GrammaGem/System/TextCapture.swift @@ -53,9 +53,7 @@ final class TextCapture { private func captureViaAccessibility() -> Capture? { guard AXIsProcessTrusted() else { return nil } - let systemWide = AXUIElementCreateSystemWide() - - guard let focused = AX.copyElement(systemWide, kAXFocusedUIElementAttribute) else { return nil } + guard let focused = AX.focusedElement() else { return nil } var valueRef: CFTypeRef? guard AXUIElementCopyAttributeValue( @@ -93,8 +91,7 @@ final class TextCapture { /// The full text value of the focused element (not just the selection). func focusedFieldText() -> (text: String, element: AXUIElement)? { guard AXIsProcessTrusted() else { return nil } - let systemWide = AXUIElementCreateSystemWide() - guard let focused = AX.copyElement(systemWide, kAXFocusedUIElementAttribute) else { return nil } + guard let focused = AX.focusedElement() else { return nil } // Only treat genuine editable text roles as fixable, so we don't report // "nothing selected" failures on sliders/steppers/custom controls.