diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index 47553bb..fd856c6 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -1,6 +1,9 @@ { "sourceLanguage" : "en", "strings" : { + "Add Automatically" : { + + }, "agent_not_running_notice_title" : { "extractionState" : "manual", "localizations" : { @@ -4323,6 +4326,9 @@ } } } + }, + "setup_ssh_add_to_config_button_%@" : { + }, "setup_ssh_added_manually_button" : { "extractionState" : "manual", @@ -5189,9 +5195,6 @@ } } } - }, - "Test" : { - }, "unnamed_secret" : { "extractionState" : "manual", diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index 8f60638..1bd57a4 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; }; 50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79324026B7600D209EA /* Preview Assets.xcassets */; }; 50A3B79724026B7600D209EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79524026B7600D209EA /* Main.storyboard */; }; + 50AE97002E5C1A420018C710 /* ConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */; }; 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; }; 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; }; 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; }; @@ -137,6 +138,7 @@ 50A3B79624026B7600D209EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 50A3B79824026B7600D209EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50A3B79924026B7600D209EA /* SecretAgent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgent.entitlements; sourceTree = ""; }; + 50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationView.swift; sourceTree = ""; }; 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 = ""; }; 50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = ""; }; @@ -251,6 +253,7 @@ 506772C82425BB8500034DED /* NoStoresView.swift */, 50153E1F250AFCB200525160 /* UpdateView.swift */, 5066A6C12516F303004B5A36 /* SetupView.swift */, + 50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */, 5066A6C72516FE6E004B5A36 /* CopyableView.swift */, ); path = Views; @@ -443,6 +446,7 @@ 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, + 50AE97002E5C1A420018C710 /* ConfigurationView.swift in Sources */, 50153E20250AFCB200525160 /* UpdateView.swift in Sources */, 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */, 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */, diff --git a/Sources/Secretive/App.swift b/Sources/Secretive/App.swift index 177beaf..27ffeba 100644 --- a/Sources/Secretive/App.swift +++ b/Sources/Secretive/App.swift @@ -71,7 +71,7 @@ struct Secretive: App { NSWorkspace.shared.open(Constants.helpURL) } } - CommandGroup(after: .help) { + CommandGroup(before: .help) { Button(.appMenuSetupButton) { showingSetup = true } diff --git a/Sources/Secretive/Controllers/AgentStatusChecker.swift b/Sources/Secretive/Controllers/AgentStatusChecker.swift index 3c85f3f..646cb4c 100644 --- a/Sources/Secretive/Controllers/AgentStatusChecker.swift +++ b/Sources/Secretive/Controllers/AgentStatusChecker.swift @@ -24,13 +24,14 @@ import Observation } // All processes, including ones from older versions, etc - var secretAgentProcesses: [NSRunningApplication] { - NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.agentBundleID) + var allSecretAgentProcesses: [NSRunningApplication] { + NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.agentBundleID) } // The process corresponding to this instance of Secretive var instanceSecretAgentProcess: NSRunningApplication? { - let agents = secretAgentProcesses + // FIXME: CHECK VERSION + let agents = allSecretAgentProcesses for agent in agents { guard let url = agent.bundleURL else { continue } if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) { @@ -40,7 +41,6 @@ import Observation return nil } - // Whether Secretive is being run in an Xcode environment. var developmentBuild: Bool { Bundle.main.bundleURL.absoluteString.contains("/Library/Developer/Xcode") diff --git a/Sources/Secretive/Controllers/LaunchAgentController.swift b/Sources/Secretive/Controllers/LaunchAgentController.swift index a65f8b0..ab0a912 100644 --- a/Sources/Secretive/Controllers/LaunchAgentController.swift +++ b/Sources/Secretive/Controllers/LaunchAgentController.swift @@ -36,7 +36,7 @@ struct LaunchAgentController { } private func setEnabled(_ enabled: Bool) -> Bool { - let service = SMAppService.loginItem(identifier: Bundle.main.agentBundleID) + let service = SMAppService.loginItem(identifier: Bundle.agentBundleID) do { if enabled { try service.register() diff --git a/Sources/Secretive/Controllers/ShellConfigurationController.swift b/Sources/Secretive/Controllers/ShellConfigurationController.swift index 2ecb17e..2044160 100644 --- a/Sources/Secretive/Controllers/ShellConfigurationController.swift +++ b/Sources/Secretive/Controllers/ShellConfigurationController.swift @@ -4,7 +4,7 @@ import SecretKit struct ShellConfigurationController { - let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String + let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String var shellInstructions: [ShellConfigInstruction] { [ diff --git a/Sources/Secretive/Helpers/BundleIDs.swift b/Sources/Secretive/Helpers/BundleIDs.swift index de4967d..bc84add 100644 --- a/Sources/Secretive/Helpers/BundleIDs.swift +++ b/Sources/Secretive/Helpers/BundleIDs.swift @@ -1,7 +1,11 @@ import Foundation - extension Bundle { - public var agentBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "Host", with: "SecretAgent"))!} - public var hostBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "SecretAgent", with: "Host"))!} + public static var agentBundleID: String { + Bundle.main.bundleIdentifier!.replacingOccurrences(of: "Host", with: "SecretAgent") + } + + public static var hostBundleID: String { + Bundle.main.bundleIdentifier!.replacingOccurrences(of: "SecretAgent", with: "Host") + } } diff --git a/Sources/Secretive/Views/ConfigurationView.swift b/Sources/Secretive/Views/ConfigurationView.swift new file mode 100644 index 0000000..c69f0b1 --- /dev/null +++ b/Sources/Secretive/Views/ConfigurationView.swift @@ -0,0 +1,47 @@ +import SwiftUI + +struct ConfigurationView: View { + + @Binding var visible: Bool + + @State var running = true + @State var sshConfig = false + + @Environment(\.agentStatusChecker) var agentStatusChecker + + var body: some View { + VStack(spacing: 0) { + NewStepView(title: "setup_agent_title", description: "setup_agent_description") { + OnboardingButton("setup_agent_install_button", running) { + Task { + _ = await LaunchAgentController().forceLaunch() + agentStatusChecker.check() + running = agentStatusChecker.running + } + } + } + Divider() + Divider() + NewStepView(title: "setup_ssh_title", description: "setup_ssh_description") { + HStack { + OnboardingButton("setup_ssh_added_manually_button", false) { + sshConfig = true + } + OnboardingButton("Add Automatically", false) { +// let controller = ShellConfigurationController() +// if controller.addToShell(shellInstructions: selectedShellInstruction) { +// } + sshConfig = true + } + } + } + } + .background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10)) + .frame(minWidth: 500, idealWidth: 500, minHeight: 500, idealHeight: 500) + .padding() + .task { + running = agentStatusChecker.running + } + } + +} diff --git a/Sources/Secretive/Views/SecretDetailView.swift b/Sources/Secretive/Views/SecretDetailView.swift index 68a1e05..140ee97 100644 --- a/Sources/Secretive/Views/SecretDetailView.swift +++ b/Sources/Secretive/Views/SecretDetailView.swift @@ -6,8 +6,8 @@ struct SecretDetailView: View { let secret: SecretType private let keyWriter = OpenSSHPublicKeyWriter() - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID)) - + private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.agentHomePath) + var body: some View { ScrollView { Form { @@ -37,6 +37,14 @@ struct SecretDetailView: View { } +extension URL { + + static var agentHomePath: String { + URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) + } + +} + #if DEBUG struct SecretDetailView_Previews: PreviewProvider { diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift index e0a2560..0338244 100644 --- a/Sources/Secretive/Views/SetupView.swift +++ b/Sources/Secretive/Views/SetupView.swift @@ -2,6 +2,124 @@ import SwiftUI struct SetupView: View { + @Binding var visible: Bool + @Binding var setupComplete: Bool + + @State var installed = false + @State var updates = false + @State var sshConfig = false + + var body: some View { + VStack(spacing: 0) { + NewStepView(title: "setup_agent_title", description: "setup_agent_description") { + OnboardingButton("setup_agent_install_button", installed) { + Task { + await LaunchAgentController().install() + installed = true + } + } + } + Divider() + NewStepView(title: "setup_updates_title", description: "setup_updates_description") { + OnboardingButton("setup_updates_ok", false) { + Task { + updates = true + } + } + } + Divider() + NewStepView(title: "setup_ssh_title", description: "setup_ssh_description") { + HStack { + OnboardingButton("setup_ssh_added_manually_button", false) { + sshConfig = true + } + OnboardingButton("Add Automatically", false) { +// let controller = ShellConfigurationController() +// if controller.addToShell(shellInstructions: selectedShellInstruction) { +// } + sshConfig = true + } + } + } + } + .background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10)) + .frame(minWidth: 500, idealWidth: 500, minHeight: 500, idealHeight: 500) + .padding() + + } + +} + +struct OnboardingButton: View { + + let label: LocalizedStringResource + let complete: Bool + let action: () -> Void + + init(_ label: LocalizedStringResource, _ complete: Bool, action: @escaping () -> Void) { + self.label = label + self.complete = complete + self.action = action + } + + var body: some View { + Button(action: action) { + HStack(spacing: 6) { + Text(label) + if complete { + Image(systemName: "checkmark.circle.fill") + } + } + .padding(.vertical, 2) + } + .disabled(complete) + .styled + } + +} + +extension View { + + @ViewBuilder + var styled: some View { + if #available(macOS 26.0, *) { + buttonStyle(.glassProminent) + } else { + buttonStyle(.borderedProminent) + } + } + +} + +struct NewStepView: View { + + let title: LocalizedStringResource + let description: LocalizedStringResource + let actions: Content + + init(title: LocalizedStringResource, description: LocalizedStringResource, actions: () -> Content) { + self.title = title + self.description = description + self.actions = actions() + } + + var body: some View { + HStack { + VStack(alignment: .leading, spacing: 6) { + Text(title) + .bold() + Text(description) + } + Spacer(minLength: 20) + actions + } + .padding(20) + } + +} + +struct OldSetupView: View { + @State var stepIndex = 0 @Binding var visible: Bool @Binding var setupComplete: Bool @@ -61,7 +179,7 @@ struct StepView: View { Circle() .foregroundColor(.green) .frame(width: Constants.circleWidth, height: Constants.circleWidth) - Text(.setupStepCompleteSymbol) + Text("setup_step_complete_symbol") .foregroundColor(.white) .bold() } else { @@ -101,14 +219,14 @@ extension StepView { struct SetupStepView : View where Content : View { - let title: LocalizedStringResource + let title: LocalizedStringKey let image: Image - let bodyText: LocalizedStringResource - let buttonTitle: LocalizedStringResource + let bodyText: LocalizedStringKey + let buttonTitle: LocalizedStringKey let buttonAction: () -> Void let content: Content - init(title: LocalizedStringResource, image: Image, bodyText: LocalizedStringResource, buttonTitle: LocalizedStringResource, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) { + init(title: LocalizedStringKey, image: Image, bodyText: LocalizedStringKey, buttonTitle: LocalizedStringKey, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) { self.title = title self.image = image self.bodyText = bodyText @@ -145,12 +263,12 @@ struct SecretAgentSetupView: View { let buttonAction: () -> Void var body: some View { - SetupStepView(title: .setupAgentTitle, + SetupStepView(title: "setup_agent_title", image: Image(nsImage: NSApplication.shared.applicationIconImage), - bodyText: .setupAgentDescription, - buttonTitle: .setupAgentInstallButton, + bodyText: "setup_agent_description", + buttonTitle: "setup_agent_install_button", buttonAction: install) { - Text(.setupAgentActivityMonitorDescription) + Text("setup_agent_activity_monitor_description") .multilineTextAlignment(.center) } } @@ -172,12 +290,12 @@ struct SSHAgentSetupView: View { @State private var selectedShellInstruction: ShellConfigInstruction = controller.shellInstructions.first! var body: some View { - SetupStepView(title: .setupSshTitle, + SetupStepView(title: "setup_ssh_title", image: Image(systemName: "terminal"), - bodyText: .setupSshDescription, - buttonTitle: .setupSshAddedManuallyButton, + bodyText: "setup_ssh_description", + buttonTitle: "setup_ssh_added_manually_button", buttonAction: buttonAction) { - Link(.setupThirdPartyFaqLink, destination: URL(string: "https://github.com/maxgoedjen/secretive/blob/main/APP_CONFIG.md")!) + Link("setup_third_party_faq_link", destination: URL(string: "https://github.com/maxgoedjen/secretive/blob/main/APP_CONFIG.md")!) Picker(selection: $selectedShellInstruction, label: EmptyView()) { ForEach(SSHAgentSetupView.controller.shellInstructions) { instruction in Text(instruction.shell) @@ -185,8 +303,8 @@ struct SSHAgentSetupView: View { .padding() } }.pickerStyle(SegmentedPickerStyle()) - CopyableView(title: .setupSshAddToConfigButton(configPath: selectedShellInstruction.shellConfigPath), image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text) - Button(.setupSshAddForMeButton) { + CopyableView(title: "setup_ssh_add_to_config_button_\(selectedShellInstruction.shellConfigPath)", image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text) + Button("setup_ssh_add_for_me_button") { let controller = ShellConfigurationController() if controller.addToShell(shellInstructions: selectedShellInstruction) { buttonAction() @@ -216,12 +334,12 @@ struct UpdaterExplainerView: View { let buttonAction: () -> Void var body: some View { - SetupStepView(title: .setupUpdatesTitle, + SetupStepView(title: "setup_updates_title", image: Image(systemName: "dot.radiowaves.left.and.right"), - bodyText: .setupUpdatesDescription, - buttonTitle: .setupUpdatesOk, + bodyText: "setup_updates_description", + buttonTitle: "setup_updates_ok", buttonAction: buttonAction) { - Link(.setupUpdatesReadmore, destination: SetupView.Constants.updaterFAQURL) + Link("setup_updates_readmore", destination: SetupView.Constants.updaterFAQURL) } }