From ff5303f45b5a2a7ffd33c726577f068b5262b332 Mon Sep 17 00:00:00 2001 From: Utkarsh Patel Date: Tue, 19 May 2026 10:44:03 +0530 Subject: [PATCH] fix(settings): make launch-at-login reflect actual system state The toggle used @AppStorage as its source of truth, so it could disagree with macOS Login Items. If register() failed or the OS put the app in .requiresApproval, the UI showed it as on regardless. Errors were print()ed, which goes nowhere because AudioType is LSUIElement. - Source of truth is now SMAppService.mainApp.status. The toggle reads .enabled, refreshes on appear and when the app becomes active. - Failures surface inline (red caption), matching the API key fields. - .requiresApproval shows a hint and an 'Open Login Items' button that jumps straight to the right System Settings panel. - Drop the misleading UserDefaults key. --- AudioType/UI/SettingsView.swift | 61 ++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/AudioType/UI/SettingsView.swift b/AudioType/UI/SettingsView.swift index f3a310b..61a184a 100644 --- a/AudioType/UI/SettingsView.swift +++ b/AudioType/UI/SettingsView.swift @@ -4,7 +4,6 @@ import Speech import SwiftUI struct SettingsView: View { - @AppStorage("launchAtLogin") private var launchAtLogin = false @State private var selectedEngine = TranscriptionEngineType.current @State private var selectedGroqModel = GroqModel.current @State private var selectedOpenAIModel = OpenAIModel.current @@ -20,6 +19,12 @@ struct SettingsView: View { @State private var isOpenAIKeySet: Bool = OpenAIEngine.isConfigured @State private var openaiKeySaveError: String? + // Launch at login state — source of truth is SMAppService.mainApp.status + @State private var launchAtLogin: Bool = SMAppService.mainApp.status == .enabled + @State private var launchAtLoginRequiresApproval: Bool = + SMAppService.mainApp.status == .requiresApproval + @State private var launchAtLoginError: String? + var body: some View { Form { // MARK: - Engine Selection @@ -179,6 +184,24 @@ struct SettingsView: View { .onChange(of: launchAtLogin) { newValue in setLaunchAtLogin(newValue) } + + if launchAtLoginRequiresApproval { + VStack(alignment: .leading, spacing: 4) { + Text("Approval needed in System Settings → Login Items.") + .font(.caption) + .foregroundColor(.secondary) + Button("Open Login Items") { + openLoginItemsSettings() + } + .font(.caption) + } + } + + if let error = launchAtLoginError { + Text(error) + .font(.caption) + .foregroundColor(.red) + } } header: { Text("General") } @@ -227,6 +250,16 @@ struct SettingsView: View { } .formStyle(.grouped) .frame(width: 400, height: 680) + .onAppear { + refreshLaunchAtLoginStatus() + } + .onReceive( + NotificationCenter.default.publisher( + for: NSApplication.didBecomeActiveNotification + ) + ) { _ in + refreshLaunchAtLoginStatus() + } } // MARK: - Shared API key field @@ -308,14 +341,34 @@ struct SettingsView: View { } private func setLaunchAtLogin(_ enabled: Bool) { + launchAtLoginError = nil + let service = SMAppService.mainApp do { if enabled { - try SMAppService.mainApp.register() + try service.register() } else { - try SMAppService.mainApp.unregister() + try service.unregister() } } catch { - print("Failed to set launch at login: \(error)") + launchAtLoginError = "Couldn't update login item: \(error.localizedDescription)" + } + // Always re-sync from the system after attempting a change so the toggle + // reflects reality (including the .requiresApproval case where register() + // succeeds but macOS is waiting on user consent). + refreshLaunchAtLoginStatus() + } + + private func refreshLaunchAtLoginStatus() { + let status = SMAppService.mainApp.status + launchAtLogin = (status == .enabled) + launchAtLoginRequiresApproval = (status == .requiresApproval) + } + + private func openLoginItemsSettings() { + if let url = URL( + string: "x-apple.systempreferences:com.apple.LoginItems-Settings.extensionPoint" + ) { + NSWorkspace.shared.open(url) } } }