fix: Microphone permission never prompted on first launch - #42
Merged
Conversation
Root cause: Two bugs working together: 1. AudioEngine.checkPermission() treated .notDetermined as .denied (both returned false), so AppState could never distinguish first launch from an explicit denial. 2. performStartup() called checkPermissions() but never called requestMicrophonePermission(), so the system prompt was never triggered. Changes: - AudioEngine: Replace boolean checkPermission() with tri-state checkPermissionStatus() returning PermissionStatus (.notDetermined, .granted, .denied) - AppState: checkPermissions() now preserves .notDetermined state; performStartup() auto-prompts for mic permission when notDetermined; requestMicrophonePermission() opens System Settings when denied (re-requesting won't show the prompt again) - SettingsView: PermissionRow shows 'Grant' (orange) for notDetermined, 'Open Settings' (red) for denied, with help text for denied state - MenuBarView: Popover permission buttons adapt labels and colors based on denied vs notDetermined state
Always create a fresh temporary event tap when checking Input Monitoring permission. The previous approach checked the existing HotKeyManager tap, but macOS doesn't immediately disable existing taps when the user revokes Input Monitoring — the change only takes effect on app restart. A fresh tap creation always reflects the current permission state.
Use .cghidEventTap (same tap type as HotKeyManager) instead of .cgSessionEventTap for permission checks and auto-registration. .cgSessionEventTap can inherit Terminal's permissions when the app is launched from CLI, giving false positives. Also restore Strategy 1: check if HotKeyManager's existing tap has been disabled by macOS (which happens on permission revocation).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On first launch via DMG, the microphone permission prompt is never shown. The app immediately marks mic permission as
deniedand logs it that way — without ever asking the user. Once in this state, there's no way to re-trigger the system prompt.Root Cause
Two bugs working together:
AudioEngine.checkPermission()treated.notDeterminedas.denied— Both cases returnedfalse, soAppStatecould never distinguish a first launch from an explicit denial.performStartup()never calledrequestMicrophonePermission()— It only calledcheckPermissions(), which silently marked mic as denied without triggering the system prompt.Fix
AudioEngine
checkPermission(completion:)with tri-statecheckPermissionStatus() -> PermissionStatusthat properly returns.notDetermined,.granted, or.deniedAppState
checkPermissions()now preserves.notDeterminedstateperformStartup()auto-callsrequestMicrophonePermission()when status is.notDetermined— this triggers the system prompt on first launchrequestMicrophonePermission()opens System Settings when already.denied(re-requesting won't show the prompt again)openMicrophoneSettings()helper that opens the Microphone privacy pane directlySettings UI
PermissionRownow shows three states:Popover