# How to Integrate FluidVoice with System-Wide Hotkeys: A Complete Implementation Guide

> Learn how to integrate FluidVoice with system-wide hotkeys. Implement global shortcuts effectively using the GlobalHotkeyManager and CGEvent tap for seamless control.

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

---

**FluidVoice captures system-wide key and mouse events through the `GlobalHotkeyManager` service using a low-level CGEvent tap that runs on the main run-loop, enabling global shortcuts even when the app is not frontmost.**

This guide explains how FluidVoice implements macOS system-wide hotkeys to start dictation, trigger command mode, and control transcription workflows from anywhere on your Mac. You'll learn how the event tap system works, how to configure `HotkeyShortcut` definitions, and how to customize activation modes to match your workflow preferences.

## How Global Hotkeys Work in FluidVoice

FluidVoice's hotkey system operates through three coordinated components in the [altic-dev/FluidVoice](https://github.com/altic-dev/FluidVoice) codebase:

- **`GlobalHotkeyManager`** – Creates and manages the CGEvent tap that intercepts system-wide keyboard and mouse events
- **`HotkeyShortcut`** – Defines the key codes, modifier flags, and mouse buttons that trigger actions
- **`SettingsStore`** – Persists user preferences and provides the central configuration interface

When FluidVoice launches, `GlobalHotkeyManager.init` schedules `initializeWithDelay()`, which attempts to set up the event tap up to five times while verifying that the app has accessibility permissions (`AXIsProcessTrusted`). Without this permission, macOS blocks the global event tap creation.

## Setting Up the Global Event Tap

The core of system-wide hotkey integration happens in [`Sources/Fluid/Services/GlobalHotkeyManager.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/GlobalHotkeyManager.swift). Here's how the initialization flow works:

1. **Permission check** – Verify `AXIsProcessTrusted` status
2. **Tap creation** – Call `CGEvent.tapCreate` with masks for key-down, key-up, flag-changed, and mouse-button events
3. **Run-loop integration** – Add the tap source to `RunLoop.main`
4. **Event dispatch** – Route captured events to `handleKeyEvent`

If the tap cannot be enabled, the manager logs an error and aborts rather than fail silently.

```swift
// Simplified initialization pattern from GlobalHotkeyManager.swift
let hotkeyManager = GlobalHotkeyManager(
    asrService: asrService,
    primaryShortcuts: SettingsStore.shared.primaryDictationShortcuts,
    promptModeShortcut: SettingsStore.shared.promptModeHotkeyShortcut,
    commandModeShortcut: SettingsStore.shared.commandModeHotkeyShortcut,
    rewriteModeShortcut: SettingsStore.shared.rewriteModeHotkeyShortcut,
    promptShortcutAssignments: [],
    promptModeShortcutEnabled: true,
    commandModeShortcutEnabled: true,
    rewriteModeShortcutEnabled: true,
    startRecordingCallback: {
        await asrService.startRecording()
    },
    commandModeCallback: {
        await asrService.startCommandMode()
    },
    rewriteModeCallback: {
        await asrService.startRewriteMode()
    }
)

```

The callbacks connect the low-level event system to FluidVoice's ASR (automatic speech recognition) service, enabling real-time transcription control.

## Defining Hotkey Shortcuts with HotkeyShortcut

A `HotkeyShortcut` struct in [`Sources/Fluid/Models/HotkeyShortcut.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Models/HotkeyShortcut.swift) encodes three essential properties:

- **`keyCode`** – The hardware key code (e.g., `0` for "A", `3` for "F")
- **`modifierFlags`** – NSEvent.ModifierFlags combination (`.control`, `.option`, `.command`, `.shift`)
- **`mouseButton`** – Alternative to keyCode for mouse-driven shortcuts

### Keyboard Shortcuts

```swift
import Fluid

// Control+Option+A to start dictation
let recordHotkey = HotkeyShortcut(
    keyCode: 0,
    modifierFlags: [.control, .option]
)

// Command+F for command mode
let commandHotkey = HotkeyShortcut(
    keyCode: 3,
    modifierFlags: [.command]
)

// Persist to SettingsStore
SettingsStore.shared.primaryDictationShortcuts = [recordHotkey]
SettingsStore.shared.commandModeShortcut = commandHotkey

```

### Mouse Shortcuts

Mouse buttons work alongside keyboard modifiers for hardware-button triggers:

```swift
// Control+side mouse button (button 3)
let sideClickHotkey = HotkeyShortcut(
    mouseButton: 3,
    modifierFlags: [.control]
)

SettingsStore.shared.primaryDictationShortcuts.append(sideClickHotkey)
hotkeyManager.updatePrimaryShortcuts(SettingsStore.shared.primaryDictationShortcuts)

```

## Choosing a Hotkey Activation Mode

FluidVoice supports three `HotkeyActivationMode` behaviors, stored in `SettingsStore.shared.hotkeyMode`:

### Hold Mode

Press and hold to start, release to stop. The manager sets `isCommandModeKeyPressed = true` on `keyDown`, triggers `triggerCommandMode()`, then on `keyUp` clears the flag and calls `stopRecordingAfterRelease`.

Best for: Momentary actions like push-to-talk dictation.

```swift
SettingsStore.shared.hotkeyMode = .hold

```

### Toggle Mode

Single press switches state on/off. The manager toggles on each `keyDown` without tracking releases.

Best for: Actions you want to leave running while typing elsewhere.

```swift
SettingsStore.shared.hotkeyMode = .toggle
hotkeyManager.hotkeyMode = SettingsStore.shared.hotkeyMode

```

### Automatic Mode

A quick tap starts the action; a second tap (or extended hold) ends it after a 0.4-second timeout. On `keyDown`, the manager calls `beginAutomaticPress` and records the press time. On release, `handleAutomaticKeyRelease` determines if the press was a clean tap or a hold.

Best for: Flexible workflows where you want both tap and hold behaviors.

```swift
SettingsStore.shared.hotkeyMode = .automatic

```

## Handling Modifier-Only Shortcuts

FluidVoice supports modifier-only shortcuts (e.g., "Option+Shift" with no regular key) through specialized logic in `ModifierOnlyShortcutFlagsDecision.evaluate`. This pure, side-effect-free function determines whether a modifier-only press has started, finished, or should be ignored.

This enables using modifiers as **hold-to-record** keys without interfering with normal typing. The manager tracks currently pressed modifier key codes in `pressedModifierKeyCodes`, allowing combinations like holding Option+Shift to activate dictation while preserving Option-Shift-character combinations for regular input.

## Event Processing Order

When `handleKeyEvent` receives a CGEvent, it parses the raw event into a key code and `NSEvent.ModifierFlags`, then checks shortcuts in this priority order:

1. **Cancel** – Immediate termination of current operation
2. **Paste last transcription** – Insert previous result
3. **Prompt assignment** – Assign prompt to shortcut slot
4. **Prompt mode** – Enter prompt selection interface
5. **Command mode** – Execute voice commands
6. **Rewrite mode** – Transform selected text
7. **Primary transcription shortcuts** – Start/stop dictation

Each check may initiate a hold, automatic press, or toggle operation based on the current `hotkeyMode` value.

## Runtime Hotkey Updates

FluidVoice allows changing shortcuts without restarting. After modifying `SettingsStore.shared`, call the appropriate update method:

```swift
// Update primary transcription shortcuts
hotkeyManager.updatePrimaryShortcuts(SettingsStore.shared.primaryDictationShortcuts)

// Update mode-specific shortcuts
hotkeyManager.updateCommandModeShortcut(SettingsStore.shared.commandModeShortcut)
hotkeyManager.updateRewriteModeShortcut(SettingsStore.shared.rewriteModeShortcut)

```

These methods reconfigure the internal shortcut matching without recreating the event tap, ensuring zero-downtime updates.

## Key Source Files

| File | Purpose | Direct Link |
|------|---------|-------------|
| [`GlobalHotkeyManager.swift`](https://github.com/altic-dev/FluidVoice/blob/main/GlobalHotkeyManager.swift) | Event tap setup, mode logic, callback dispatch | [View on GitHub](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/GlobalHotkeyManager.swift) |
| [`HotkeyShortcut.swift`](https://github.com/altic-dev/FluidVoice/blob/main/HotkeyShortcut.swift) | Shortcut model, conversion helpers, matching | [View on GitHub](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Models/HotkeyShortcut.swift) |
| [`SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/SettingsStore.swift) | Persisted preferences, hotkey storage | [View on GitHub](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Models/SettingsStore.swift) |
| [`SettingsView.swift`](https://github.com/altic-dev/FluidVoice/blob/main/SettingsView.swift) | UI for editing shortcuts | [View on GitHub](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/UI/SettingsView.swift) |
| [`fluidApp.swift`](https://github.com/altic-dev/FluidVoice/blob/main/fluidApp.swift) | Application entry, manager instantiation | [View on GitHub](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/fluidApp.swift) |

## Summary

- **Global hotkeys require accessibility permissions** – macOS blocks the CGEvent tap without `AXIsProcessTrusted`
- **`GlobalHotkeyManager`** creates a system-wide event tap that intercepts key and mouse events even when FluidVoice is backgrounded
- **`HotkeyShortcut`** defines triggers through key codes, modifier flags, or mouse buttons
- **Three activation modes** – hold, toggle, and automatic – adapt to different interaction patterns
- **Modifier-only shortcuts** work through `ModifierOnlyShortcutFlagsDecision` for non-interfering hold-to-record behavior
- **Runtime updates** via `updatePrimaryShortcuts()` and related methods reconfigure without restart

## Frequently Asked Questions

### Why doesn't my hotkey work immediately after installing FluidVoice?

macOS requires explicit accessibility permissions for global event taps. FluidVoice checks `AXIsProcessTrusted` and retries initialization up to five times, but you must manually grant permission in **System Settings > Privacy & Security > Accessibility**. Without this, `CGEvent.tapCreate` fails and the global hotkey system cannot activate.

### Can I use the same shortcut for different modes in different contexts?

No. FluidVoice processes shortcuts in a fixed priority order (cancel → paste → prompt assignment → prompt mode → command mode → rewrite mode → primary). The first matching shortcut wins. For context-specific behavior, modify `SettingsStore.shared.*Enabled` flags to disable unwanted modes, or use `updatePrimaryShortcuts()` to swap configurations dynamically.

### How do I find the correct keyCode for a specific keyboard key?

Key codes are hardware-specific scan codes, not character codes. The most reliable method is using a keycode detection utility or referencing NSEvent's `keyCode` property in a test app. Common values include: `0` (A), `3` (F), `36` (Return), `49` (Space), `53` (Escape). Mouse buttons use separate numbering: `1` (left), `2` (right), `3` (side), etc.

### Does FluidVoice support complex chord shortcuts like "Command+K, Command+S"?

No. `HotkeyShortcut` only supports single-beat shortcuts: one key or mouse button plus modifiers. Sequential chords would require extending `GlobalHotkeyManager` to track pending states across event boundaries. Current implementation focuses on immediate-activation shortcuts for low-latency voice control.