Implement Phase 1: Global hotkey, HUD, and audio capture

Add complete listening UX without STT:
- Global hotkey manager with ⌘⇧V, push-to-talk and toggle modes
- Floating HUD with real-time RMS audio visualization
- AVAudioEngine capture with 16kHz mono PCM conversion
- 10-minute dictation timeout with ESC cancellation
- Optional start/stop sounds and microphone permissions
- Permission management for accessibility and input monitoring

All Phase 1 acceptance criteria met.
This commit is contained in:
Felipe M 2025-09-18 20:06:46 +02:00
parent 1db16227b2
commit 6e768a7753
Signed by: fmartingr
GPG key ID: CCFBC5637D4000A8
10 changed files with 1005 additions and 51 deletions

View file

@ -50,16 +50,58 @@ public class PermissionManager: ObservableObject {
}
}
public func requestMicrophonePermission(completion: @escaping (PermissionStatus) -> Void) {
logger.info("Requesting microphone permission")
switch AVCaptureDevice.authorizationStatus(for: .audio) {
case .authorized:
completion(.granted)
case .denied, .restricted:
completion(.denied)
case .notDetermined:
AVCaptureDevice.requestAccess(for: .audio) { granted in
let status: PermissionStatus = granted ? .granted : .denied
Task { @MainActor in
self.microphoneStatus = status
}
completion(status)
}
@unknown default:
completion(.notDetermined)
}
}
public func requestAccessibilityPermission() {
logger.info("Requesting accessibility permission")
// TODO: Implement accessibility permission request in Phase 1
// This typically involves guiding the user to System Settings
if !AXIsProcessTrusted() {
logger.info("Accessibility permission not granted, opening System Settings")
openSystemSettings(for: .accessibility)
} else {
logger.info("Accessibility permission already granted")
accessibilityStatus = .granted
}
}
public func requestInputMonitoringPermission() {
logger.info("Requesting input monitoring permission")
// TODO: Implement input monitoring permission request in Phase 1
// This typically involves guiding the user to System Settings
// For input monitoring, we can try to detect it by attempting to create a CGEvent
// If it fails, we likely need permission
let testEvent = CGEvent(keyboardEventSource: nil, virtualKey: 0, keyDown: true)
if testEvent == nil {
logger.info("Input monitoring permission likely not granted, opening System Settings")
openSystemSettings(for: .inputMonitoring)
} else {
logger.info("Input monitoring permission appears to be granted")
inputMonitoringStatus = .granted
}
}
public func checkAllPermissions() {
logger.info("Checking all permissions")
refreshAllPermissions()
}
public func openSystemSettings(for permission: PermissionType) {
@ -100,12 +142,21 @@ public class PermissionManager: ObservableObject {
}
private func refreshAccessibilityPermission() {
// TODO: Implement accessibility permission check in Phase 1
accessibilityStatus = .notDetermined
if AXIsProcessTrusted() {
accessibilityStatus = .granted
} else {
accessibilityStatus = .denied
}
}
private func refreshInputMonitoringPermission() {
// TODO: Implement input monitoring permission check in Phase 1
inputMonitoringStatus = .notDetermined
// Test if we can create CGEvents (requires Input Monitoring permission)
let testEvent = CGEvent(keyboardEventSource: nil, virtualKey: 0, keyDown: true)
if testEvent != nil {
inputMonitoringStatus = .granted
} else {
inputMonitoringStatus = .denied
}
}
}