From 6a1d58d4e7578bc0b1d1d6d3efe69ade5490d3a6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 6 Dec 2025 02:41:33 +0100 Subject: [PATCH] mac: fix voice wake mic picker build --- apps/macos/Sources/Clawdis/AppMain.swift | 47 ++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/apps/macos/Sources/Clawdis/AppMain.swift b/apps/macos/Sources/Clawdis/AppMain.swift index b04e72f72..e2ce59daa 100644 --- a/apps/macos/Sources/Clawdis/AppMain.swift +++ b/apps/macos/Sources/Clawdis/AppMain.swift @@ -23,6 +23,7 @@ private let pauseDefaultsKey = "clawdis.pauseEnabled" private let swabbleEnabledKey = "clawdis.swabbleEnabled" private let swabbleTriggersKey = "clawdis.swabbleTriggers" private let defaultVoiceWakeTriggers = ["clawd", "claude"] +private let voiceWakeMicKey = "clawdis.voiceWakeMicID" // MARK: - App model @@ -56,6 +57,9 @@ final class AppState: ObservableObject { } } } + @Published var voiceWakeMicID: String { + didSet { UserDefaults.standard.set(voiceWakeMicID, forKey: voiceWakeMicKey) } + } init() { self.isPaused = UserDefaults.standard.bool(forKey: pauseDefaultsKey) @@ -65,6 +69,7 @@ final class AppState: ObservableObject { self.debugPaneEnabled = UserDefaults.standard.bool(forKey: "clawdis.debugPaneEnabled") self.swabbleEnabled = UserDefaults.standard.bool(forKey: swabbleEnabledKey) self.swabbleTriggerWords = UserDefaults.standard.stringArray(forKey: swabbleTriggersKey) ?? defaultVoiceWakeTriggers + self.voiceWakeMicID = UserDefaults.standard.string(forKey: voiceWakeMicKey) ?? "" } } @@ -1567,6 +1572,14 @@ struct VoiceWakeSettings: View { @State private var testState: VoiceWakeTestState = .idle @State private var tester = VoiceWakeTester() @State private var isTesting = false + @State private var availableMics: [AudioInputDevice] = [] + @State private var loadingMics = false + + struct AudioInputDevice: Identifiable, Hashable { + let uid: String + let name: String + var id: String { uid } + } private struct IndexedWord: Identifiable { let id: Int @@ -1581,6 +1594,8 @@ struct VoiceWakeSettings: View { binding: $state.swabbleEnabled ) + micPicker + testCard VStack(alignment: .leading, spacing: 8) { @@ -1631,6 +1646,7 @@ struct VoiceWakeSettings: View { } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 12) + .task { await loadMicsIfNeeded() } } private var indexedWords: [IndexedWord] { @@ -1769,6 +1785,37 @@ struct VoiceWakeSettings: View { .filter { !$0.isEmpty } return cleaned.isEmpty ? defaultVoiceWakeTriggers : cleaned } + + private var micPicker: some View { + VStack(alignment: .leading, spacing: 6) { + LabeledContent("Microphone") { + Picker("Microphone", selection: $state.voiceWakeMicID) { + Text("System default").tag("") + ForEach(availableMics) { mic in + Text(mic.name).tag(mic.uid) + } + } + .labelsHidden() + .frame(width: 260) + } + if loadingMics { + ProgressView().controlSize(.small) + } + } + } + + @MainActor + private func loadMicsIfNeeded() async { + guard availableMics.isEmpty, !loadingMics else { return } + loadingMics = true + let discovery = AVCaptureDevice.DiscoverySession( + deviceTypes: [.external, .microphone], + mediaType: .audio, + position: .unspecified + ) + availableMics = discovery.devices.map { AudioInputDevice(uid: $0.uniqueID, name: $0.localizedName) } + loadingMics = false + } } struct PermissionsSettings: View {