From d9165e1e4decb6d2e151e3a4ff3b2d2b1e205792 Mon Sep 17 00:00:00 2001 From: Beingpax Date: Mon, 30 Jun 2025 20:11:20 +0545 Subject: [PATCH] Fix selected text detection and Power Mode handling --- .../Services/PromptDetectionService.swift | 15 ++++- VoiceInk/Services/SelectedTextService.swift | 67 ++++++++++--------- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/VoiceInk/Services/PromptDetectionService.swift b/VoiceInk/Services/PromptDetectionService.swift index 66b7ca3..a2fcbe9 100644 --- a/VoiceInk/Services/PromptDetectionService.swift +++ b/VoiceInk/Services/PromptDetectionService.swift @@ -19,7 +19,18 @@ class PromptDetectionService { func analyzeText(_ text: String, with enhancementService: AIEnhancementService) -> PromptDetectionResult { let originalEnhancementState = enhancementService.isEnhancementEnabled let originalPromptId = enhancementService.selectedPromptId - + + if let selectedText = SelectedTextService.fetchSelectedText(), !selectedText.isEmpty { + return PromptDetectionResult( + shouldEnableAI: true, + selectedPromptId: PredefinedPrompts.assistantPromptId, + processedText: text, // The user's speech is the prompt for the selected text + detectedTriggerWord: nil, + originalEnhancementState: originalEnhancementState, + originalPromptId: originalPromptId + ) + } + for prompt in enhancementService.allPrompts { if !prompt.triggerWords.isEmpty { if let (detectedWord, processedText) = findMatchingTriggerWord(from: text, triggerWords: prompt.triggerWords) { @@ -34,7 +45,7 @@ class PromptDetectionService { } } } - + return PromptDetectionResult( shouldEnableAI: false, selectedPromptId: nil, diff --git a/VoiceInk/Services/SelectedTextService.swift b/VoiceInk/Services/SelectedTextService.swift index 97a66c3..2da682c 100644 --- a/VoiceInk/Services/SelectedTextService.swift +++ b/VoiceInk/Services/SelectedTextService.swift @@ -3,49 +3,50 @@ 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, - let element = focusedElement else { + 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 } - return findSelectedText(in: element as! AXUIElement) - } - - private static func findSelectedText(in element: AXUIElement) -> String? { - var selectedTextValue: CFTypeRef? - if AXUIElementCopyAttributeValue(element, kAXSelectedTextAttribute as CFString, &selectedTextValue) == .success { - if let selectedText = selectedTextValue as? String, !selectedText.isEmpty { - return selectedText - } + 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 } - // Fallback for apps that use kAXValueAttribute for selected text (like some Electron apps) - var value: CFTypeRef? - if AXUIElementCopyAttributeValue(element, kAXValueAttribute as CFString, &value) == .success { - if let selectedText = value as? String, !selectedText.isEmpty { - return selectedText - } - } - - var children: CFTypeRef? - if AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &children) == .success { - if let axChildren = children as? [AXUIElement] { - for child in axChildren { - if let foundText = findSelectedText(in: child) { - return foundText - } - } - } - } - - return nil + return String(fullText[subrange]) } } \ No newline at end of file