# How FluidVoice Rewrite Mode Works with Text Selection in Any macOS Application

> Discover how FluidVoice rewrite mode uses macOS Accessibility to capture selected text, process it with an LLM, and insert AI-generated replacements into any application.

- Repository: [ALTIC/FluidVoice](https://github.com/altic-dev/FluidVoice)
- Tags: how-to-guide
- Published: 2026-07-03

---

**FluidVoice's rewrite mode captures the currently selected text from any foreground macOS application using the Accessibility framework, processes it through an LLM, and types the AI-generated replacement back into the original app.**

The **FluidVoice** open-source project (`altic-dev/FluidVoice`) enables users to rewrite text across any macOS application without requiring specialized integrations or plugins. By leveraging system-level Accessibility APIs, the app reads selected text from the foreground application's UI hierarchy, generates rewritten content via an LLM, and automatically replaces the original selection using simulated keyboard input.

## How Rewrite Mode Activates and Captures Selected Text

When a user invokes the global hotkey, FluidVoice initiates a capture sequence that bridges the UI layer with low-level system services. The `ContentView.handleModeTransition()` method signals `RewriteModeService` to begin the capture process, which immediately queries the `TextSelectionService` to extract the current selection.

The entry point in [`RewriteModeService.swift`](https://github.com/altic-dev/FluidVoice/blob/main/RewriteModeService.swift) handles the coordination between the view layer and the text extraction logic:

```swift
// In RewriteModeService.swift
func captureSelectedText() -> Bool {
    if let text = textSelectionService.getSelectedText(), !text.isEmpty {
        self.originalText = text                // UI will show this
        self.selectedContextText = text
        self.rewrittenText = ""
        self.conversationHistory = []
        self.isWriteMode = false                // we are in rewrite mode
        return true
    }
    return false
}

```

If `captureSelectedText()` returns `true`, the `RewriteModeView` displays the captured string in the **Original Text** panel, preparing the user to enter rewrite instructions.

## Accessibility Framework Implementation for Cross-App Text Selection

FluidVoice operates across arbitrary applications by treating the macOS Accessibility framework as a universal interface to the UI hierarchy. The `TextSelectionService.getSelectedText()` method performs a two-tier lookup: first targeting the system-wide focused element, then falling back to the frontmost application if necessary.

### Primary Text Extraction via kAXSelectedTextAttribute

The service attempts to read the `kAXSelectedTextAttribute` directly from the focused accessibility element. This attribute provides immediate access to highlighted text without requiring manual substring calculations.

### Fallback Extraction Using Range and Value Attributes

When an application does not expose `kAXSelectedTextAttribute`, FluidVoice implements a robust fallback strategy using `kAXSelectedTextRangeAttribute` and `kAXValueAttribute`. This approach reads the full text content of the element, retrieves the selected range as a `CFRange`, and extracts the corresponding substring.

The implementation in [`TextSelectionService.swift`](https://github.com/altic-dev/FluidVoice/blob/main/TextSelectionService.swift) demonstrates this dual-path extraction:

```swift
// Inside TextSelectionService.swift
private func getSelectedText(from element: AXUIElement) -> String? {
    var value: CFTypeRef?
    let result = AXUIElementCopyAttributeValue(element,
                                               kAXSelectedTextAttribute as CFString,
                                               &value)
    if result == .success, let text = value as? String {
        return text
    }

    // --- Fallback: use range + full value ---
    var rangeRef: CFTypeRef?
    let rangeResult = AXUIElementCopyAttributeValue(element,
                                                    kAXSelectedTextRangeAttribute as CFString,
                                                    &rangeRef)
    guard rangeResult == .success, let axRange = rangeRef else { return nil }

    var range = CFRange()
    guard AXValueGetValue(axRange as! AXValue, .cfRange, &range) else { return nil }
    guard range.length > 0 else { return nil }

    var fullValueRef: CFTypeRef?
    let fullResult = AXUIElementCopyAttributeValue(element,
                                                   kAXValueAttribute as CFString,
                                                   &fullValueRef)
    guard fullResult == .success, let fullText = fullValueRef as? String else { return nil }

    let nsText = fullText as NSString
    return nsText.substring(with: NSRange(location: range.location, length: range.length))
}

```

This fallback mechanism ensures compatibility with applications that implement accessibility differently, including browsers, code editors, and native macOS text fields.

## Processing Rewrites with LLM Integration

Once the original text is captured and stored in `RewriteModeService.originalText` and `selectedContextText`, the service enters **Rewrite Mode** (as opposed to **Write Mode**, which generates content from scratch). The `processRewriteRequest(_:)` method constructs a prompt that combines the user's instruction with the selected context, then dispatches it to the LLM via `LLMClient`.

```swift
// In RewriteModeService.swift
func processRewriteRequest(_ prompt: String) async {
    // Build the prompt that includes the selected context
    if !self.originalText.isEmpty {                 // rewrite mode
        let rewritePrompt = """
        User's instruction: \(prompt)

        Apply the instruction to the selected context. Output ONLY the rewritten text, nothing else.
        """
        self.conversationHistory.append(Message(role: .user, content: rewritePrompt))
    } else {                                         // write mode
        self.originalText = prompt
        self.isWriteMode = true
        self.conversationHistory.append(Message(role: .user, content: prompt))
    }

    // Call the LLM (non‑streaming for edit mode)
    let response = try await callLLM(messages: conversationHistory,
                                     isWriteMode: isWriteMode)
    self.rewrittenText = response
}

```

The method distinguishes between rewrite and write modes by checking `originalText.isEmpty`. When text exists, it constructs a constrained prompt instructing the LLM to output only the rewritten content without additional commentary.

## Injecting Rewritten Text Back into the Target Application

After the LLM returns the rewritten text and the user accepts the changes, `RewriteModeService.acceptRewrite()` handles the final injection. The method hides the FluidVoice window to return focus to the target application, then uses `TypingService.typeTextInstantly(_:)` to simulate keyboard input and replace the original selection.

```swift
func acceptRewrite() {
    guard !self.rewrittenText.isEmpty else { return }
    NSApp.hide(nil)                         // return focus to the target app
    typingService.typeTextInstantly(self.rewrittenText)
}

```

This approach operates entirely through standard input simulation, requiring no clipboard manipulation or application-specific APIs.

## Required macOS Accessibility Permissions

FluidVoice requires the user to grant **Accessibility** permissions for the app to read and interact with other applications' UI elements. The codebase checks `AXIsProcessTrusted()` to verify permissions before attempting accessibility operations, logging diagnostics if access is denied.

All text extraction and injection operations depend on this single permission grant, making the architecture both powerful and simple to deploy across different macOS environments.

## Summary

- **FluidVoice** uses the macOS Accessibility framework to read selected text from any foreground application via `TextSelectionService.getSelectedText()`.
- The system attempts direct extraction via `kAXSelectedTextAttribute`, falling back to `kAXSelectedTextRangeAttribute` and `kAXValueAttribute` when necessary.
- `RewriteModeService` coordinates the capture, LLM processing, and injection workflow, storing state in `originalText` and `selectedContextText`.
- Rewritten content is injected back into the source application using `TypingService.typeTextInstantly(_:)`, which simulates keyboard input after `NSApp.hide(nil)` returns focus.
- The feature requires macOS Accessibility permissions (`AXIsProcessTrusted()`) but needs no application-specific integration or plugins.

## Frequently Asked Questions

### Does FluidVoice rewrite mode work with all macOS applications?

FluidVoice's rewrite mode works with any macOS application that implements standard accessibility attributes for text fields. The `TextSelectionService` handles variations in implementation by checking for `kAXSelectedTextAttribute` first, then falling back to range-based extraction using `kAXSelectedTextRangeAttribute` and `kAXValueAttribute`. Applications that completely disable accessibility access or use non-standard text rendering may block this functionality.

### What happens if no text is selected when activating rewrite mode?

If `captureSelectedText()` returns `false` due to empty selection, `RewriteModeService` automatically switches to **Write Mode** by setting `isWriteMode = true` and treating the user's input as a generation request rather than a rewrite. The `processRewriteRequest(_:)` method detects an empty `originalText` property and constructs a generative prompt instead of a context-aware rewrite instruction.

### How does FluidVoice handle privacy when reading text from other apps?

FluidVoice reads text directly from the foreground application's UI hierarchy using the Accessibility framework, processing it locally through the `RewriteModeService` before sending it to the configured LLM endpoint. The text never passes through the system clipboard during the capture or replacement phases. Users maintain control over when the hotkey activates the capture mechanism, and the app checks `AXIsProcessTrusted()` to ensure permissions are explicitly granted.

### Can I customize the LLM prompt used for rewriting text?

While the provided code shows a fixed prompt template in `processRewriteRequest(_:)`, the architecture supports customization through the `conversationHistory` array and the `callLLM(messages:isWriteMode:)` method. Developers can modify the prompt construction logic in [`RewriteModeService.swift`](https://github.com/altic-dev/FluidVoice/blob/main/RewriteModeService.swift) to include additional constraints, formatting instructions, or system messages before dispatching to the `LLMClient`.