How to Integrate FluidVoice with System-Wide Hotkeys: A Complete Implementation Guide
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 codebase:
GlobalHotkeyManager– Creates and manages the CGEvent tap that intercepts system-wide keyboard and mouse eventsHotkeyShortcut– Defines the key codes, modifier flags, and mouse buttons that trigger actionsSettingsStore– 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. Here's how the initialization flow works:
- Permission check – Verify
AXIsProcessTrustedstatus - Tap creation – Call
CGEvent.tapCreatewith masks for key-down, key-up, flag-changed, and mouse-button events - Run-loop integration – Add the tap source to
RunLoop.main - Event dispatch – Route captured events to
handleKeyEvent
If the tap cannot be enabled, the manager logs an error and aborts rather than fail silently.
// 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 encodes three essential properties:
keyCode– The hardware key code (e.g.,0for "A",3for "F")modifierFlags– NSEvent.ModifierFlags combination (.control,.option,.command,.shift)mouseButton– Alternative to keyCode for mouse-driven shortcuts
Keyboard Shortcuts
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:
// 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.
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.
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.
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:
- Cancel – Immediate termination of current operation
- Paste last transcription – Insert previous result
- Prompt assignment – Assign prompt to shortcut slot
- Prompt mode – Enter prompt selection interface
- Command mode – Execute voice commands
- Rewrite mode – Transform selected text
- 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:
// 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 |
Event tap setup, mode logic, callback dispatch | View on GitHub |
HotkeyShortcut.swift |
Shortcut model, conversion helpers, matching | View on GitHub |
SettingsStore.swift |
Persisted preferences, hotkey storage | View on GitHub |
SettingsView.swift |
UI for editing shortcuts | View on GitHub |
fluidApp.swift |
Application entry, manager instantiation | View on GitHub |
Summary
- Global hotkeys require accessibility permissions – macOS blocks the CGEvent tap without
AXIsProcessTrusted GlobalHotkeyManagercreates a system-wide event tap that intercepts key and mouse events even when FluidVoice is backgroundedHotkeyShortcutdefines 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
ModifierOnlyShortcutFlagsDecisionfor 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →