How FluidVoice Inserts Transcribed Text Into Other Applications: A Deep Dive Into the macOS Typing Pipeline

FluidVoice turns speech-to-text results into typed content in the user's active window using a multi-stage pipeline that preserves the clipboard, tries the most reliable insertion methods first, and falls back to lower-level techniques when needed.

FluidVoice is an open-source macOS dictation tool from altic-dev/FluidVoice that bridges the gap between speech recognition and application input. When you finish speaking, the transcribed text doesn't just appear—it gets injected into whichever app currently has focus. The core mechanism lives in Sources/Fluid/Services/TypingService.swift, where the TypingService class orchestrates a sophisticated fallback chain to ensure text insertion works across diverse application types.

The TypingService Architecture

The TypingService class serves as the abstraction layer between raw transcribed strings and the target application. Its design prioritizes clipboard preservation and method reliability over speed, ensuring users never lose their existing clipboard contents.

Pipeline Overview

Every text insertion follows this sequence:

  1. Snapshot the pasteboard — Save current clipboard state
  2. Create a transient pasteboard item — Tag text as auto-generated and temporary
  3. Attempt ordered insertion strategies — Try methods from most to least reliable
  4. Restore original pasteboard — Return clipboard to previous state

This pipeline executes atomically through the typeTextInstantly(_:) method, the primary entry point for UI layers.

Step 1: Clipboard Preservation

Before any text injection occurs, FluidVoice safeguards user data. The capturePasteboardSnapshot(_:) method in TypingService.swift (line 604) creates a complete backup of the general pasteboard. After insertion completes—success or failure—the restorePasteboardSnapshot(_:, to:) method (line 638) returns the clipboard to its original state.

// Simplified preservation flow
let snapshot = capturePasteboardSnapshot(.general)

// ... insertion attempts happen here ...

restorePasteboardSnapshot(snapshot, to: .general)

This ensures that a user's previously copied password, image, or text remains intact after dictation.

Step 2: Transient Pasteboard Creation

FluidVoice marks its clipboard usage as non-persistent. The makeTransientPasteboardItem(_:) method (line 638) constructs an NSPasteboardItem annotated with two critical type identifiers:

  • org.nspasteboard.TransientType — Indicates this data should not appear in clipboard history
  • org.nspasteboard.AutoGeneratedType — Signals programmatic origin
static func makeTransientPasteboardItem(_ string: String) -> NSPasteboardItem {
    let item = NSPasteboardItem()
    item.setString(string, forType: .string)
    item.setData(Data(), forType: NSPasteboard.PasteboardType("org.nspasteboard.TransientType"))
    item.setData(Data(), forType: NSPasteboard.PasteboardType("org.nspasteboard.AutoGeneratedType"))
    return item
}

These annotations prevent dictation results from polluting third-party clipboard managers and history tools.

Step 3: The Six-Stage Insertion Strategy

FluidVoice implements a cascading fallback system that tries insertion methods in descending order of reliability and user experience quality. Each method is isolated in its own function for testability and debugging.

3a. Clipboard-to-PID Paste (Most Reliable)

The insertViaClipboardToPID(_:) method (line 699) targets a specific process identifier with a synthetic Cmd+V keystroke. By scoping the paste event to the target PID rather than the global event stream, this method avoids race conditions where another app might intercept the paste.

// Pseudocode showing PID-scoped paste concept
func insertViaClipboardToPID(_ text: String, _ pid: pid_t) -> Bool {
    // Transient item already on pasteboard
    let source = CGEventSource(stateID: .hidSystemState)
    let keyDown = CGEvent(keyboardEventSource: source, virtualKey: 0x09, keyDown: true) // Cmd+V down
    let keyUp = CGEvent(keyboardEventSource: source, virtualKey: 0x09, keyDown: false) // Cmd+V up
    
    keyDown?.flags = .maskCommand
    keyUp?.flags = .maskCommand
    
    // Target specific process
    keyDown?.postToPid(pid)
    keyUp?.postToPid(pid)
    
    return verifyInsertionSuccess()
}

This method works with most native Cocoa and Carbon applications.

3b. Global Clipboard Paste

When PID-specific posting fails, insertViaGlobalClipboard(_:) (line 822) falls back to a traditional global paste event. This broader approach reaches more applications but carries higher risk of misfires if focus changes during execution.

3c. Menu-Based Paste

For applications with non-standard event handling, insertViaMenuPaste(_:) (line 844) executes the target's "Paste" menu command through AppleScript:

-- Generated by TypingService for target application
tell application "System Events"
    tell process "TargetApp"
        click menu item "Paste" of menu "Edit" of menu bar 1
    end tell
end tell

This method respects application-specific paste behaviors but requires Accessibility permissions and runs slower than direct events.

3d. CGEvent Keyboard Injection

The insertViaCGEventToPID(_:) and insertViaCGEventHID(_:) methods (line 736) bypass the clipboard entirely. They generate low-level Core Graphics keyboard events that directly simulate physical keypresses typing the string character by character.

Two variants exist:

  • PID-specific: Targets a process directly via CGEvent.postToPid()
  • HID: Uses the hardware input device path for applications that block process-targeted events

This approach works where clipboard-based methods fail, though it's slower for long text and may struggle with Unicode characters.

3e. Accessibility API Insertion

The insertViaAccessibility(_:) method (line 878) walks the macOS accessibility hierarchy to find a text-compatible UI element (AXTextArea, AXTextField, etc.) and sets its AXValue attribute directly:

func insertViaAccessibility(_ text: String) -> Bool {
    let systemWide = AXUIElementCreateSystemWide()
    
    // Get focused element
    var focusedElement: AXUIElement?
    AXUIElementCopyAttributeValue(systemWide, kAXFocusedUIElementAttribute as CFString, &focusedElement)
    
    guard let element = focusedElement else { return false }
    
    // Verify text support
    var role: CFString?
    AXUIElementCopyAttributeValue(element, kAXRoleAttribute as CFString, &role)
    
    // Set value directly
    AXUIElementSetAttributeValue(element, kAXValueAttribute as CFString, text as CFString)
    return true
}

This method requires Accessibility permissions but succeeds in sandboxed, security-sensitive, or non-standard apps (including some web browsers and terminal emulators).

3f. Character-by-Character Fallback

When all higher-level approaches fail, insertViaCharacterByCharacter(_:) (line 487) sends individual key events for every character. While slow and prone to timing issues with modifier keys, this lowest-common-denominator method functions even in restricted environments.

Complete Usage Example

Here's how the UI layer invokes the full pipeline:

import Fluid

// Transcription result from speech recognizer
let transcript = "Meeting rescheduled to 3pm tomorrow."

// Single call triggers entire insertion pipeline
TypingService.shared.typeTextInstantly(transcript)

Internally, this executes the sequence:

func typeTextInstantly(_ text: String) {
    // Capture current clipboard
    let snapshot = capturePasteboardSnapshot(.general)
    
    // Prepare transient content
    NSPasteboard.general.clearContents()
    NSPasteboard.general.writeObjects([Self.makeTransientPasteboardItem(text)])
    
    // Attempt insertion cascade
    var success = false
    if let pid = getTargetPID() {
        success = insertViaClipboardToPID(text, pid)
    }
    if !success { success = insertViaGlobalClipboard(text) }
    if !success { success = insertViaMenuPaste(text) }
    if !success { success = insertViaCGEventToPID(text, targetPID) }
    if !success { success = insertViaAccessibility(text) }
    if !success { insertViaCharacterByCharacter(text) }
    
    // Always restore original clipboard
    restorePasteboardSnapshot(snapshot, to: .general)
}

Supporting Infrastructure

File Function
Sources/Fluid/Services/TypingService.swift Core insertion engine with six fallback strategies
Sources/Fluid/Services/ClipboardService.swift Lightweight pasteboard wrapper for basic read/write
Sources/Fluid/Views/NotchContentViews.swift UI layer calling TypingService.activateApp(pid:) for focus management
Tests/FluidDictationIntegrationTests/TypingServiceTransientPasteboardTests.swift Unit tests validating transient pasteboard behavior

Summary

  • FluidVoice inserts transcribed text through a robust six-stage pipeline in TypingService.swift, prioritizing user clipboard preservation above all else
  • Transient pasteboard items prevent dictation results from appearing in clipboard history
  • PID-specific paste methods provide the most reliable injection for standard applications
  • Accessibility and character-by-character fallbacks ensure functionality across sandboxed, secure, or non-standard apps
  • Complete clipboard restoration guarantees users never lose existing copied content

Frequently Asked Questions

Does FluidVoice replace my clipboard contents when dictating?

No. FluidVoice captures your clipboard state before insertion and restores it afterward. The transcribed text is written to a transient pasteboard item tagged as auto-generated, which most clipboard managers ignore. Your original copied content remains available after dictation completes.

Why does FluidVoice need Accessibility permissions?

The Accessibility API insertion method (insertViaAccessibility) requires these permissions to enumerate UI elements and set text values directly. This fallback activates when standard paste commands fail—common in Terminal, certain browsers, and sandboxed applications. Without this permission, FluidVoice degrades to slower, less reliable character-by-character typing.

How does FluidVoice handle applications that block standard paste commands?

The pipeline automatically degrades through multiple strategies. If PID-scoped and global clipboard pastes fail, FluidVoice tries AppleScript menu execution, then low-level CGEvent keyboard injection, then direct Accessibility API manipulation, and finally individual keystroke simulation. Each method targets different application security models and event-handling architectures.

Can I see which insertion method was used for debugging?

Yes. The TypingService class integrates with DebugLogger throughout the pipeline. Enable debug logging in FluidVoice's settings to see method attempts, success/failure states, and timing metrics for each insertion. This data appears in Console.app under the com.altic.FluidVoice subsystem.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →