52 lines
2.3 KiB
Swift
52 lines
2.3 KiB
Swift
import Foundation
|
|
import AppKit
|
|
|
|
class SelectedTextService {
|
|
static func fetchSelectedText() -> String? {
|
|
// Do not check for selected text within VoiceInk itself.
|
|
guard let frontmostApp = NSWorkspace.shared.frontmostApplication,
|
|
frontmostApp.bundleIdentifier != "com.prakashjoshipax.VoiceInk" else {
|
|
return nil
|
|
}
|
|
|
|
// Get the currently focused UI element system-wide.
|
|
let systemWideElement = AXUIElementCreateSystemWide()
|
|
var focusedElement: CFTypeRef?
|
|
guard AXUIElementCopyAttributeValue(systemWideElement, kAXFocusedUIElementAttribute as CFString, &focusedElement) == .success, focusedElement != nil else {
|
|
return nil
|
|
}
|
|
let element = focusedElement as! AXUIElement
|
|
|
|
// First, try the standard attribute, which is the most reliable method.
|
|
var selectedTextValue: CFTypeRef?
|
|
if AXUIElementCopyAttributeValue(element, kAXSelectedTextAttribute as CFString, &selectedTextValue) == .success,
|
|
let selectedText = selectedTextValue as? String, !selectedText.isEmpty {
|
|
return selectedText
|
|
}
|
|
|
|
// If the standard attribute fails, fall back to checking the selection range.
|
|
// This correctly handles apps that don't support the standard attribute.
|
|
var selectedRangeValue: CFTypeRef?
|
|
guard AXUIElementCopyAttributeValue(element, kAXSelectedTextRangeAttribute as CFString, &selectedRangeValue) == .success, selectedRangeValue != nil else {
|
|
return nil
|
|
}
|
|
let axRangeValue = selectedRangeValue as! AXValue
|
|
|
|
var selectedRange: CFRange = .init()
|
|
AXValueGetValue(axRangeValue, .cfRange, &selectedRange)
|
|
|
|
// An actual selection must have a length greater than zero.
|
|
guard selectedRange.length > 0 else {
|
|
return nil
|
|
}
|
|
|
|
var fullTextValue: CFTypeRef?
|
|
guard AXUIElementCopyAttributeValue(element, kAXValueAttribute as CFString, &fullTextValue) == .success,
|
|
let fullText = fullTextValue as? String,
|
|
let subrange = Range(NSRange(location: selectedRange.location, length: selectedRange.length), in: fullText) else {
|
|
return nil
|
|
}
|
|
|
|
return String(fullText[subrange])
|
|
}
|
|
} |