From cd76bb95ecfef9f92f3f923a01ebe4bf0abe42f0 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sun, 31 Aug 2025 00:58:16 -0700 Subject: [PATCH] Tweaks. --- Sources/Secretive.xcodeproj/project.pbxproj | 4 + .../Controllers/LaunchAgentController.swift | 9 +- Sources/Secretive/Views/AgentStatusView.swift | 183 +++++++++++------- Sources/Secretive/Views/ContentView.swift | 8 +- .../Secretive/Views/CreateSecretView.swift | 32 ++- .../Secretive/Views/DeleteSecretView.swift | 3 +- Sources/Secretive/Views/EditSecretView.swift | 10 +- Sources/Secretive/Views/ErrorStyle.swift | 19 ++ 8 files changed, 177 insertions(+), 91 deletions(-) create mode 100644 Sources/Secretive/Views/ErrorStyle.swift diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index b09a304..8dd813b 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -52,6 +52,7 @@ 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; }; 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; }; 50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */; }; + 50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */; }; 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; }; 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; }; /* End PBXBuildFile section */ @@ -142,6 +143,7 @@ 50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = ""; }; 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = ""; }; 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusView.swift; sourceTree = ""; }; + 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorStyle.swift; sourceTree = ""; }; 50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = ""; }; 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -259,6 +261,7 @@ 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */, 50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */, 5066A6C72516FE6E004B5A36 /* CopyableView.swift */, + 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */, ); path = Views; sourceTree = ""; @@ -436,6 +439,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */, 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */, 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */, 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, diff --git a/Sources/Secretive/Controllers/LaunchAgentController.swift b/Sources/Secretive/Controllers/LaunchAgentController.swift index c863d92..308c381 100644 --- a/Sources/Secretive/Controllers/LaunchAgentController.swift +++ b/Sources/Secretive/Controllers/LaunchAgentController.swift @@ -15,17 +15,21 @@ struct LaunchAgentController { // Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old // and start new? try? await Task.sleep(for: .seconds(1)) - return await MainActor.run { + let result = await MainActor.run { setEnabled(true) } + try? await Task.sleep(for: .seconds(1)) + return result } func uninstall() async -> Bool { logger.debug("Uninstalling agent") try? await Task.sleep(for: .seconds(1)) - return await MainActor.run { + let result = await MainActor.run { setEnabled(false) } + try? await Task.sleep(for: .seconds(1)) + return result } func forceLaunch() async -> Bool { @@ -36,6 +40,7 @@ struct LaunchAgentController { do { try await NSWorkspace.shared.openApplication(at: url, configuration: config) logger.debug("Agent force launched") + try? await Task.sleep(for: .seconds(1)) return true } catch { logger.error("Error force launching \(error.localizedDescription)") diff --git a/Sources/Secretive/Views/AgentStatusView.swift b/Sources/Secretive/Views/AgentStatusView.swift index b829a15..eab0ea0 100644 --- a/Sources/Secretive/Views/AgentStatusView.swift +++ b/Sources/Secretive/Views/AgentStatusView.swift @@ -3,81 +3,63 @@ import SwiftUI struct AgentStatusView: View { @Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol - private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String var body: some View { if agentStatusChecker.running { - Form { - Section { - if let process = agentStatusChecker.process { + AgentRunningView() + } else { + AgentNotRunningView() + } + } +} +struct AgentRunningView: View { + + @Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol + private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String + + var body: some View { + Form { + Section { + if let process = agentStatusChecker.process { + AgentInformationView( + title: "Secret Agent Location", + value: process.bundleURL!.path(), + actions: [.revealInFinder], + ) + AgentInformationView( + title: "Socket Path", + value: socketPath, + actions: [.copy], + ) + AgentInformationView( + title: "Version", + value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String + ) + if let launchDate = process.launchDate { AgentInformationView( - title: "Secret Agent Location", - value: process.bundleURL!.path(), - actions: [.revealInFinder], + title: "Running Since", + value: launchDate.formatted() ) - AgentInformationView( - title: "Socket Path", - value: socketPath, - actions: [.copy], - ) - AgentInformationView( - title: "Version", - value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String - ) - if let launchDate = process.launchDate { - AgentInformationView( - title: "Running Since", - value: launchDate.formatted() - ) - } } - } header: { - Text(.agentRunningNoticeDetailTitle) - .font(.headline) - .padding(.top) - } footer: { - VStack(alignment: .leading) { - Text(.agentRunningNoticeDetailDescription) - HStack { - Spacer() - Menu("Restart Agent") { - Button("Disable Agent") { - Task { - await LaunchAgentController() - .uninstall() - } - } - } primaryAction: { + } + } header: { + Text(.agentRunningNoticeDetailTitle) + .font(.headline) + .padding(.top) + } footer: { + VStack(alignment: .leading, spacing: 10) { + Text(.agentRunningNoticeDetailDescription) + HStack { + Spacer() + Menu("Restart Agent") { + Button("Disable Agent") { Task { - let controller = LaunchAgentController() - let installed = await controller.install() - if !installed { - _ = await controller.forceLaunch() - } + _ = await LaunchAgentController() + .uninstall() agentStatusChecker.check() } } - } - } - .padding(.vertical) - } - - } - .formStyle(.grouped) - .frame(width: 400) - } else { - Form { - Section { - } header: { - Text(.agentNotRunningNoticeTitle) - .font(.headline) - .padding(.top) - } footer: { - Text(.agentNotRunningNoticeDetailDescription) - Spacer() - HStack { - Spacer() - Button("Start Agent") { + } primaryAction: { Task { let controller = LaunchAgentController() let installed = await controller.install() @@ -87,14 +69,77 @@ struct AgentStatusView: View { agentStatusChecker.check() } } - .primary() } - .padding(.vertical) } + .padding(.vertical) } - .formStyle(.grouped) - .frame(width: 400) + } + .formStyle(.grouped) + .frame(width: 400) + } + +} + +struct AgentNotRunningView: View { + + @Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol + @State var triedRestart = false + @State var loading = false + + var body: some View { + Form { + Section { + } header: { + Text(.agentNotRunningNoticeTitle) + .font(.headline) + .padding(.top) + } footer: { + VStack(alignment: .leading, spacing: 10) { + Text(.agentNotRunningNoticeDetailDescription) + HStack { + if !triedRestart { + Spacer() + Button { + guard !loading else { return } + loading = true + Task { + let controller = LaunchAgentController() + let installed = await controller.install() + if !installed { + _ = await controller.forceLaunch() + } + agentStatusChecker.check() + loading = false + + if !agentStatusChecker.running { + triedRestart = true + } + } + } label: { + if !loading { + Text("Start Agent") + } else { + HStack { + Text("Starting Agent") + ProgressView() + .controlSize(.mini) + } + } + } + .primary() + } else { + Text("Secretive was unable to get SecretAgent to launch. Please try restarting your Mac, and if that doesn't work, file an issue on GitHub.") + .bold() + .foregroundStyle(.red) + } + } + } + .padding(.bottom) + } + } + .formStyle(.grouped) + .frame(width: 400) } } diff --git a/Sources/Secretive/Views/ContentView.swift b/Sources/Secretive/Views/ContentView.swift index 34e0405..29d8a35 100644 --- a/Sources/Secretive/Views/ContentView.swift +++ b/Sources/Secretive/Views/ContentView.swift @@ -110,11 +110,11 @@ extension ContentView { }) .sheet(isPresented: $showingCreation) { if let modifiable = storeList.modifiableStore { - CreateSecretView(store: modifiable, showing: $showingCreation) - .onDisappear { - guard let newest = modifiable.secrets.last else { return } - activeSecret = newest + CreateSecretView(store: modifiable, showing: $showingCreation) { created in + if let created { + activeSecret = created } + } } } } diff --git a/Sources/Secretive/Views/CreateSecretView.swift b/Sources/Secretive/Views/CreateSecretView.swift index 7173042..9a5557d 100644 --- a/Sources/Secretive/Views/CreateSecretView.swift +++ b/Sources/Secretive/Views/CreateSecretView.swift @@ -5,12 +5,14 @@ struct CreateSecretView: View { @State var store: StoreType @Binding var showing: Bool + var createdSecret: (AnySecret?) -> Void @State private var name = "" @State private var keyAttribution = "" @State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired @State private var keyType: KeyType? @State var advanced = false + @State var errorText: String? private var authenticationOptions: [AuthenticationRequirement] { if advanced || authenticationRequirement == .biometryCurrent { @@ -94,6 +96,13 @@ struct CreateSecretView: View { } } } + if let errorText { + Section { + } footer: { + Text(verbatim: errorText) + .errorStyle() + } + } } HStack { Toggle(.createSecretAdvancedLabel, isOn: $advanced) @@ -118,20 +127,25 @@ struct CreateSecretView: View { func save() { let attribution = keyAttribution.isEmpty ? nil : keyAttribution Task { - try! await store.create( - name: name, - attributes: .init( - keyType: keyType!, - authentication: authenticationRequirement, - publicKeyAttribution: attribution + do { + let new = try await store.create( + name: name, + attributes: .init( + keyType: keyType!, + authentication: authenticationRequirement, + publicKeyAttribution: attribution + ) ) - ) - showing = false + createdSecret(AnySecret(new)) + showing = false + } catch { + errorText = error.localizedDescription + } } } } #Preview { - CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true)) + CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true)) { _ in } } diff --git a/Sources/Secretive/Views/DeleteSecretView.swift b/Sources/Secretive/Views/DeleteSecretView.swift index 2deee63..17f6610 100644 --- a/Sources/Secretive/Views/DeleteSecretView.swift +++ b/Sources/Secretive/Views/DeleteSecretView.swift @@ -28,8 +28,7 @@ struct DeleteSecretConfirmationModifier: ViewModifier { TextField(secret.name, text: $confirmedSecretName) if let errorText { Text(verbatim: errorText) - .foregroundStyle(.red) - .font(.callout) + .errorStyle() } Button(.deleteConfirmationDeleteButton, action: delete) .disabled(confirmedSecretName != secret.name) diff --git a/Sources/Secretive/Views/EditSecretView.swift b/Sources/Secretive/Views/EditSecretView.swift index 12be6ad..06dde39 100644 --- a/Sources/Secretive/Views/EditSecretView.swift +++ b/Sources/Secretive/Views/EditSecretView.swift @@ -30,11 +30,11 @@ struct EditSecretView: View { .font(.subheadline) .foregroundStyle(.secondary) } - } - if let errorText { - Text(verbatim: errorText) - .foregroundStyle(.red) - .font(.callout) + } footer: { + if let errorText { + Text(verbatim: errorText) + .errorStyle() + } } } HStack { diff --git a/Sources/Secretive/Views/ErrorStyle.swift b/Sources/Secretive/Views/ErrorStyle.swift new file mode 100644 index 0000000..18917f1 --- /dev/null +++ b/Sources/Secretive/Views/ErrorStyle.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct ErrorStyleModifier: ViewModifier { + + func body(content: Content) -> some View { + content + .foregroundStyle(.red) + .font(.callout) + } + +} + +extension View { + + func errorStyle() -> some View { + modifier(ErrorStyleModifier()) + } + +}