FluidVoice Global Hotkey Event Tap System Architecture: A Deep Dive

FluidVoice implements a self-contained, retry-aware event-tap pipeline using macOS Core Graphics APIs, organized into three logical layers: tap creation with run-loop integration, robustness and health-check mechanisms, and event dispatch with a thread-safe state machine.

The FluidVoice project (altic-dev/FluidVoice) is an open-source macOS dictation application that requires system-wide keyboard monitoring to function. Its global hotkey event tap system captures keyboard and mouse events across the entire macOS environment, enabling features like push-to-talk transcription, command mode activation, and prompt-based rewriting. Understanding this architecture reveals how modern macOS applications can implement reliable, always-on input monitoring while handling system-imposed constraints and race conditions.

Three-Layer Architecture Overview

The event tap system in GlobalHotkeyManager.swift separates concerns into distinct architectural layers that handle creation, resilience, and business logic.

Layer 1: Event-Tap Creation and Run-Loop Integration

This layer establishes the low-level communication channel with macOS. The setupGlobalHotkey() method creates a system-wide event tap using CGEvent.tapCreate, configuring it to listen for keyboard, mouse, and modifier flag changes. The tap wraps in a CFMachPortCreateRunLoopSource and registers with the main run loop via CFRunLoopGetMain(), ensuring events flow into the application’s event stream【/cache/repos/github.com/altic-dev/FluidVoice/main/Sources/Fluid/Services/GlobalHotkeyManager.swift#L62-L88】.

Layer 2: Robustness and Health-Checking

macOS automatically disables event taps under specific conditions—timeouts or user input protection mechanisms. The handleTapDisableEvent(_:type:event:) callback detects .tapDisabledByTimeout and .tapDisabledByUserInput states, immediately re-enabling the tap【/cache/repos/github.com/altic-dev/FluidVoice/main/Sources/Fluid/Services/GlobalHotkeyManager.swift#L94-L112】. For catastrophic failures, setupGlobalHotkeyWithRetry() implements an exponential back-off strategy, attempting recreation up to maxRetryAttempts (default 5) before failing gracefully【/cache/repos/github.com/altic-dev/FluidVoice/main/Sources/Fluid/Services/GlobalHotkeyManager.swift#L42-L50】.

Layer 3: Event Dispatch and State Machine

Raw CGEvent objects transform into semantic actions through handleKeyEvent(proxy:type:event:). This layer parses keyDown, keyUp, flagsChanged, and mouse events, routing them to specialized handlers like primaryModifierOnlyBehavior and handlePromptModeKeyDown. Mutable state lives in a dedicated HotkeyState container protected by NSLock, ensuring thread safety when the background event-tap thread modifies flags consumed by the main thread【/cache/repos/github.com/altic-dev/FluidVoice/main/Sources/Fluid/Services/GlobalHotkeyManager.swift#L200-L260】.

Core Components and Data Structures

Three primary data structures orchestrate the global hotkey event tap system:

  • GlobalHotkeyManager: The central coordinator owning the eventTap reference, runLoopSource, and callback closures. It manages the lifecycle from initialization through teardown and exposes the public API for configuring shortcuts.

  • HotkeyState: A lock-protected struct marked as @unchecked Sendable that holds volatile flags including isKeyPressed, modifierOnlyKeyDown, and pendingHoldModeStart. This structure eliminates race conditions between the Core Graphics callback thread and the UI thread【/cache/repos/github.com/altic-dev/FluidVoice/main/Sources/Fluid/Services/GlobalHotkeyManager.swift#L45-L62】.

  • HotkeyShortcut: Encapsulates concrete keyboard combinations and mouse gestures with modifier masks. This model provides matching algorithms, conflict detection, and user-facing display strings, residing in Sources/Fluid/Models/HotkeyShortcut.swift【/cache/repos/github.com/altic-dev/FluidVoice/main/Sources/Fluid/Models/HotkeyShortcut.swift#L5-L95】.

Event Handling Modes

The architecture supports three distinct interaction patterns for transcription and command modes:

Hold Mode
Press-and-hold behavior initiates on keyDown and terminates on keyUp. The system tracks the duration through HotkeyState.isKeyPressed, ensuring the action stops immediately upon release.

Automatic Mode
This hybrid approach uses automaticTapThresholdSeconds (approximately 0.4 seconds) to distinguish between brief taps and extended holds. The beginAutomaticPress method starts a timer; if release occurs before the threshold, it triggers a tap action via finishAutomaticPress, otherwise it transitions to hold logic via handleAutomaticKeyRelease.

Toggle Mode
A single keystroke alternates between active and inactive states. The implementation checks asrService.isRunning to determine whether to invoke stopAndProcessCallback or startRecordingCallback, requiring no additional state tracking beyond the service’s running flag.

Modifier-Only Shortcuts
When users press only modifier keys (Command, Option, Control), the handleModifierOnlyShortcutFlagsChanged handler creates a pending task stored in HotkeyState.pendingHoldModeStart. If no additional keys arrive within the hold delay, the system executes the onHoldStart closure; otherwise, cancelPendingModifierOnlyHoldStart aborts the operation.

Implementation Details

Creating the Event Tap

The initialization sequence delays tap creation by 1.5 seconds to allow the system to grant Accessibility permissions. The setupGlobalHotkey() function configures the tap to listen for mouse, key, and flags changed events:

// From GlobalHotkeyManager.swift
func setupGlobalHotkey() {
    // Create the event tap for keyboard, mouse, and flags changed events
    let eventMask = (1 << CGEventType.keyDown.rawValue) | 
                    (1 << CGEventType.keyUp.rawValue) | 
                    (1 << CGEventType.flagsChanged.rawValue) |
                    (1 << CGEventType.mouseMoved.rawValue)
    
    guard let tap = CGEvent.tapCreate(
        tap: .cgSessionEventTap,
        place: .headInsertEventTap,
        options: .defaultTap,
        eventsOfInterest: CGEventMask(eventMask),
        callback: { proxy, type, event, refcon in
            // Handle event
            return Unmanaged.passRetained(event)
        },
        userInfo: nil
    ) else {
        return
    }
    
    self.eventTap = tap
    self.runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
    CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .defaultMode)
    CGEvent.tapEnable(tap: tap, enable: true)
}

Retry Logic and Robustness

The retry mechanism wraps the setup logic to handle transient failures during permissions requests or system resource constraints:

// Exponential backoff with maxRetryAttempts (default 5)
func setupGlobalHotkeyWithRetry(attempt: Int = 0) {
    setupGlobalHotkey()
    
    if eventTap == nil && attempt < maxRetryAttempts {
        let delay = pow(2.0, Double(attempt)) // 1, 2, 4, 8, 16 seconds
        DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
            self.setupGlobalHotkeyWithRetry(attempt: attempt + 1)
        }
    }
}

Thread Safety and State Management

The event tap callback executes on a background thread, requiring synchronization for shared state. The HotkeyState struct uses NSLock to protect mutable properties:

struct HotkeyState {
    private let lock = NSLock()
    
    private var _isKeyPressed: Bool = false
    var isKeyPressed: Bool {
        get { lock.withLock { _isKeyPressed } }
        set { lock.withLock { _isKeyPressed = newValue } }
    }
    
    // Additional state: pendingHoldModeStart, modifierOnlyKeyDown, etc.
}

Code Examples

Initializing the Global Hotkey Manager

Applications configure the manager by providing shortcut definitions and callbacks for transcription lifecycle events:

let hotkeyMgr = GlobalHotkeyManager(
    asrService: asrService,
    primaryShortcuts: SettingsStore.shared.transcribeHotkeys,
    promptModeShortcut: SettingsStore.shared.promptHotkey,
    commandModeShortcut: SettingsStore.shared.commandHotkey,
    rewriteModeShortcut: SettingsStore.shared.rewriteHotkey,
    promptShortcutAssignments: SettingsStore.shared.promptAssignments,
    promptModeShortcutEnabled: true,
    commandModeShortcutEnabled: true,
    rewriteModeShortcutEnabled: true,
    startRecordingCallback: { await asrService.start() },
    stopAndProcessCallback: { await asrService.stopAndProcess() }
)

Handling Modifier-Only Shortcuts

The modifier-only detection logic tracks flags-changed events separately from standard keystrokes:

func handleModifierOnlyShortcutFlagsChanged(_ flags: CGEventFlags) {
    let isModifierActive = flags.contains(.maskCommand) || 
                           flags.contains(.maskOption)
    
    if isModifierActive && !hotkeyState.modifierOnlyKeyDown {
        hotkeyState.modifierOnlyKeyDown = true
        hotkeyState.pendingHoldModeStart = DispatchWorkItem {
            self.activateHoldMode()
        }
        DispatchQueue.main.asyncAfter(
            deadline: .now() + holdThreshold, 
            execute: hotkeyState.pendingHoldModeStart!
        )
    } else if !isModifierActive {
        cancelPendingModifierOnlyHoldStart()
        hotkeyState.modifierOnlyKeyDown = false
    }
}

Simulating Events for Testing

The test suite exercises the event pipeline by injecting synthetic CGEvent objects:

let testShortcut = HotkeyShortcut(keyCode: 0, modifierFlags: .maskCommand)
let event = CGEvent(
    keyboardEventSource: nil, 
    virtualKey: CGKeyCode(testShortcut.keyCode), 
    keyDown: true
)!

hotkeyMgr.handleKeyEvent(
    proxy: .default, 
    type: .keyDown, 
    event: event
)

Key Source Files

The global hotkey event tap system spans the following files in the altic-dev/FluidVoice repository:

File Purpose
Sources/Fluid/Services/GlobalHotkeyManager.swift Core event-tap creation, retry logic, and state machine implementation
Sources/Fluid/Models/HotkeyShortcut.swift Data models for keyboard/mouse shortcuts with matching algorithms
Tests/FluidDictationIntegrationTests/HotkeyShortcutTests.swift Unit tests covering hold, automatic, and toggle modes
Sources/Fluid/Services/ASRService.swift Audio service providing isRunning state and start/stop APIs consumed by the hotkey manager

Summary

  • FluidVoice’s global hotkey event tap system uses macOS Core Graphics APIs (CGEvent.tapCreate) to monitor system-wide input events.
  • The three-layer architecture separates tap creation, robustness/retry logic, and event dispatch into distinct concerns.
  • Automatic recovery mechanisms detect when macOS disables the tap and rebuild it using exponential back-off with up to 5 retry attempts.
  • Thread-safe state management via HotkeyState and NSLock ensures race-condition-free operation between the background event-tap thread and the main application thread.
  • Multiple interaction modes (Hold, Automatic/Tap, Toggle) accommodate different user workflows for dictation and command input.

Frequently Asked Questions

How does FluidVoice handle macOS disabling the event tap?

The handleTapDisableEvent(_:type:event:) callback monitors for .tapDisabledByTimeout and .tapDisabledByUserInput event types. When detected, it immediately re-enables the tap using CGEvent.tapEnable(tap:enable:)【/cache/repos/github.com/altic-dev/FluidVoice/main/Sources/Fluid/Services/GlobalHotkeyManager.swift#L94-L112】. If the tap fails to recreate, the setupGlobalHotkeyWithRetry() method implements an exponential back-off strategy, attempting reconstruction up to five times before abandoning the effort.

What makes the hotkey state thread-safe in FluidVoice?

The HotkeyState struct isolates mutable flags using an NSLock instance. All properties like isKeyPressed and pendingHoldModeStart wrap their getters and setters with lock.withLock(), ensuring atomic access from the background thread running the Core Graphics callback and the main UI thread【/cache/repos/github.com/altic-dev/FluidVoice/main/Sources/Fluid/Services/GlobalHotkeyManager.swift#L45-L62】. This prevents race conditions when the user releases a key simultaneously with the processing of a hotkey action.

How does FluidVoice distinguish between a tap and a hold action?

The automatic mode uses a threshold timer set to approximately 0.4 seconds (automaticTapThresholdSeconds). When the user presses a key, beginAutomaticPress starts a timer. If handleAutomaticKeyRelease fires before the threshold elapses, the system treats it as a tap; otherwise, it transitions to hold mode. This logic resides in the event dispatch layer of GlobalHotkeyManager.

Where is the event tap callback registered in the run loop?

The callback registers with the main run loop through CFMachPortCreateRunLoopSource and CFRunLoopAddSource. Specifically, setupGlobalHotkey() creates a CFRunLoopSource from the mach port returned by CGEvent.tapCreate, then adds it to CFRunLoopGetMain() with the default mode, ensuring event processing occurs on the main thread despite the tap existing at the system level【/cache/repos/github.com/altic-dev/FluidVoice/main/Sources/Fluid/Services/GlobalHotkeyManager.swift#L62-L88】.

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 →