import AppKit import Combine import ServiceManagement enum RecorderState: Equatable { case idle case preparing case recording case transcribing case success(String) case failure(String) } @MainActor final class AppModel: ObservableObject { @Published private(set) var state: RecorderState = .idle @Published private(set) var level = 0.0 @Published private(set) var elapsed: TimeInterval = 0 @Published private(set) var startsAtLogin = SMAppService.mainApp.status == .enabled @Published private(set) var shortcutError: String? let settings = AppSettings() private let audioRecorder = AudioRecorder() private let transcriptionClient = TranscriptionClient() private let hotkeyManager = HotkeyManager() private lazy var overlay = OverlayPanelController(model: self) private var elapsedTimer: Timer? private var startedAt: Date? private var cancellables = Set() private var registeredShortcut: HotkeyShortcut? private var restoringShortcut = false private var noticeID = UUID() private var recordingRequestID: UUID? init() { audioRecorder.onLevel = { [weak self] level in self?.level = level } registerHotkey(settings.shortcut) settings.$shortcut .dropFirst() .sink { [weak self] shortcut in self?.registerHotkey(shortcut) } .store(in: &cancellables) } var isRecording: Bool { state == .recording || state == .preparing } var isWorking: Bool { isRecording || state == .transcribing } func toggleRecording() { switch state { case .recording: stopAndTranscribe() case .preparing: cancelRecording() case .idle, .success, .failure: Task { await startRecording() } case .transcribing: break } } func cancelRecording() { guard isRecording else { return } elapsedTimer?.invalidate() elapsedTimer = nil recordingRequestID = nil audioRecorder.cancel() state = .idle overlay.hide() } func setStartAtLogin(_ enabled: Bool) { do { if enabled { try SMAppService.mainApp.register() } else { try SMAppService.mainApp.unregister() } let status = SMAppService.mainApp.status startsAtLogin = status == .enabled if enabled, status == .requiresApproval { showFailure("Allow Transui in Login Items.") SMAppService.openSystemSettingsLoginItems() } } catch { startsAtLogin = SMAppService.mainApp.status == .enabled if isWorking { NSSound.beep() } else { showFailure(error.localizedDescription) } } } func openMicrophoneSettings() { openSystemSettings("x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone") } func openAccessibilitySettings() { openSystemSettings( "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") } private func startRecording() async { guard !transcriptionPlan.endpoints.isEmpty else { showFailure("Set a transcription endpoint in Settings.") return } state = .preparing overlay.show() let requestID = UUID() recordingRequestID = requestID do { try await audioRecorder.start() guard recordingRequestID == requestID else { audioRecorder.cancel() return } elapsed = 0 startedAt = Date() elapsedTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] _ in Task { @MainActor in guard let self, let startedAt = self.startedAt else { return } self.elapsed = Date().timeIntervalSince(startedAt) if self.elapsed >= 300 { self.stopAndTranscribe() } } } state = .recording } catch { recordingRequestID = nil audioRecorder.cancel() showFailure(error.localizedDescription) } } private func stopAndTranscribe() { guard let recording = audioRecorder.stop() else { showFailure("No recording was captured.") return } recordingRequestID = nil elapsedTimer?.invalidate() elapsedTimer = nil state = .transcribing Task { defer { try? FileManager.default.removeItem(at: recording) } do { let encoded = try await Task.detached(priority: .userInitiated) { try MP3Encoder.encode(wav: recording) }.value defer { try? FileManager.default.removeItem(at: encoded) } let text = try await transcriptionClient.transcribe( recording: encoded, plan: transcriptionPlan, prompt: settings.prompt ) let result = TranscriptOutput.deliver(text, to: settings.destination) switch result { case .typed: showSuccess("Typed at cursor") case .copied: showSuccess("Copied to clipboard") case .copiedBecauseAccessibilityIsOff: showSuccess("Copied. Accessibility is off") } } catch { showFailure(error.localizedDescription) } } } private func registerHotkey(_ shortcut: HotkeyShortcut) { if restoringShortcut { restoringShortcut = false return } if hotkeyManager.register(shortcut, action: { [weak self] in self?.toggleRecording() }) { registeredShortcut = shortcut shortcutError = nil } else { shortcutError = "That shortcut is already in use." if let registeredShortcut { restoringShortcut = true settings.shortcut = registeredShortcut } } } private var transcriptionPlan: TranscriptionPlan { TranscriptionPlan( hosted: TranscriptionEndpoint( label: "Hosted", endpoint: settings.hostedEndpoint, model: settings.hostedModel, key: settings.hostedKey ), local: TranscriptionEndpoint( label: "Local", endpoint: settings.localEndpoint, model: settings.localModel, key: settings.localKey ), hostedFirst: settings.hostedFirst ) } private func showSuccess(_ message: String) { state = .success(message) overlay.show() hideOverlayAfterDelay() } private func showFailure(_ message: String) { elapsedTimer?.invalidate() elapsedTimer = nil state = .failure(message) overlay.show() hideOverlayAfterDelay(seconds: 3.5) } private func hideOverlayAfterDelay(seconds: Double = 1.6) { let id = UUID() noticeID = id Task { try? await Task.sleep(for: .seconds(seconds)) guard noticeID == id else { return } guard case .success = state else { if case .failure = state { state = .idle overlay.hide() } return } state = .idle overlay.hide() } } private func openSystemSettings(_ value: String) { guard let url = URL(string: value) else { return } NSWorkspace.shared.open(url) } }