From cbf903deb7648a219e4ff5963e926c625ce5e8f0 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Mon, 25 Aug 2025 00:48:07 -0700 Subject: [PATCH 01/17] WIP --- Sources/Packages/Localizable.xcstrings | 9 +- Sources/Secretive.xcodeproj/project.pbxproj | 4 + Sources/Secretive/App.swift | 2 +- .../Controllers/AgentStatusChecker.swift | 8 +- .../Controllers/LaunchAgentController.swift | 2 +- .../ShellConfigurationController.swift | 2 +- Sources/Secretive/Helpers/BundleIDs.swift | 10 +- .../Secretive/Views/ConfigurationView.swift | 47 ++++++ .../Secretive/Views/SecretDetailView.swift | 12 +- Sources/Secretive/Views/SetupView.swift | 156 +++++++++++++++--- 10 files changed, 218 insertions(+), 34 deletions(-) create mode 100644 Sources/Secretive/Views/ConfigurationView.swift 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) } } From f60a44c599f1e76c9064202a3ceda9ba776d39b8 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sat, 30 Aug 2025 13:55:19 -0700 Subject: [PATCH 02/17] WIP --- Sources/Packages/Localizable.xcstrings | 14 ++- .../Secretive/Views/ConfigurationView.swift | 12 +- Sources/Secretive/Views/ContentView.swift | 2 +- .../Secretive/Views/CreateSecretView.swift | 1 + Sources/Secretive/Views/EditSecretView.swift | 7 +- Sources/Secretive/Views/SetupView.swift | 104 ++++++++++++------ Sources/Secretive/Views/UpdateView.swift | 73 ++++++++---- 7 files changed, 151 insertions(+), 62 deletions(-) diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index fd856c6..403b721 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -1,6 +1,9 @@ { "sourceLanguage" : "en", "strings" : { + ".zshrc" : { + + }, "Add Automatically" : { }, @@ -1163,6 +1166,9 @@ } } } + }, + "Configure" : { + }, "copyable_click_to_copy_button" : { "extractionState" : "manual", @@ -1500,7 +1506,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "This shows at the end of your public key." + "value" : "This shows at the end of your public key. It’s usually an email address." } } } @@ -2483,6 +2489,9 @@ } } } + }, + "Done" : { + }, "edit_cancel_button" : { "extractionState" : "manual", @@ -5195,6 +5204,9 @@ } } } + }, + "TfileDialogMessageest" : { + }, "unnamed_secret" : { "extractionState" : "manual", diff --git a/Sources/Secretive/Views/ConfigurationView.swift b/Sources/Secretive/Views/ConfigurationView.swift index c69f0b1..6d9eef7 100644 --- a/Sources/Secretive/Views/ConfigurationView.swift +++ b/Sources/Secretive/Views/ConfigurationView.swift @@ -11,7 +11,11 @@ struct ConfigurationView: View { var body: some View { VStack(spacing: 0) { - NewStepView(title: "setup_agent_title", description: "setup_agent_description") { + NewStepView( + title: "setup_agent_title", + description: "setup_agent_description", + systemImage: "network.badge.shield.half.filled", + ) { OnboardingButton("setup_agent_install_button", running) { Task { _ = await LaunchAgentController().forceLaunch() @@ -22,7 +26,11 @@ struct ConfigurationView: View { } Divider() Divider() - NewStepView(title: "setup_ssh_title", description: "setup_ssh_description") { + NewStepView( + title: "setup_ssh_title", + description: "setup_ssh_description", + systemImage: "network.badge.shield.half.filled", + ) { HStack { OnboardingButton("setup_ssh_added_manually_button", false) { sshConfig = true diff --git a/Sources/Secretive/Views/ContentView.swift b/Sources/Secretive/Views/ContentView.swift index dfc6dff..899d12b 100644 --- a/Sources/Secretive/Views/ContentView.swift +++ b/Sources/Secretive/Views/ContentView.swift @@ -94,7 +94,7 @@ extension ContentView { .foregroundColor(.white) }) .buttonStyle(ToolbarButtonStyle(color: color)) - .popover(item: $selectedUpdate, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { update in + .sheet(item: $selectedUpdate) { update in UpdateDetailView(update: update) } } diff --git a/Sources/Secretive/Views/CreateSecretView.swift b/Sources/Secretive/Views/CreateSecretView.swift index b5f17b5..7173042 100644 --- a/Sources/Secretive/Views/CreateSecretView.swift +++ b/Sources/Secretive/Views/CreateSecretView.swift @@ -103,6 +103,7 @@ struct CreateSecretView: View { showing = false } Button(.createSecretCreateButton, action: save) + .keyboardShortcut(.return) .primary() .disabled(name.isEmpty) } diff --git a/Sources/Secretive/Views/EditSecretView.swift b/Sources/Secretive/Views/EditSecretView.swift index 606b130..12be6ad 100644 --- a/Sources/Secretive/Views/EditSecretView.swift +++ b/Sources/Secretive/Views/EditSecretView.swift @@ -38,13 +38,14 @@ struct EditSecretView: View { } } HStack { - Button(.editSaveButton, action: rename) - .disabled(name.isEmpty) - .keyboardShortcut(.return) Button(.editCancelButton) { dismissalBlock(false) } .keyboardShortcut(.cancelAction) + Button(.editSaveButton, action: rename) + .disabled(name.isEmpty) + .keyboardShortcut(.return) + .primary() } .padding() } diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift index 0338244..030caf3 100644 --- a/Sources/Secretive/Views/SetupView.swift +++ b/Sources/Secretive/Views/SetupView.swift @@ -10,44 +10,73 @@ struct SetupView: View { @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 + VStack { + VStack(spacing: 0) { + NewStepView( + title: "setup_agent_title", + description: "setup_agent_description", + systemImage: "lock.laptopcomputer", + ) { + OnboardingButton("setup_agent_install_button", installed) { + Task { + await LaunchAgentController().install() + installed = true + } + } + } + Divider() + NewStepView( + title: "setup_updates_title", + description: "setup_updates_description", + systemImage: "network.badge.shield.half.filled", + ) { + OnboardingButton("setup_updates_ok", false) { + Task { + updates = true + } + } + } + Divider() + NewStepView( + title: "setup_ssh_title", + description: "setup_ssh_description", + systemImage: "network.badge.shield.half.filled", + ) { + HStack { + OnboardingButton("setup_ssh_added_manually_button", false) { + sshConfig = true + } + OnboardingButton("Add Automatically", false) { + // let controller = ShellConfigurationController() + // if controller.addToShell(shellInstructions: selectedShellInstruction) { + // } + sshConfig = true + } + .fileImporter(isPresented: $sshConfig, allowedContentTypes: [.utf8PlainText, .symbolicLink, .data]) { result in + print(result) + } + // FIXME: + .fileDialogDefaultDirectory(URL(fileURLWithPath: "/Users/max/")) + .fileDialogBrowserOptions([.displayFileExtensions, .includeHiddenFiles]) + .fileExporterFilenameLabel(Text(".zshrc")) + .fileDialogMessage("TfileDialogMessageest") + .fileDialogConfirmationLabel("Configure") + .fileDialogURLEnabled(#Predicate { + return $0.lastPathComponent == ".zshrc" || $0.hasDirectoryPath + }) } } } - 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: 700, maxWidth: .infinity) + HStack { + Spacer() + Button("Done") {} + .styled } } - .background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10)) - .frame(minWidth: 500, idealWidth: 500, minHeight: 500, idealHeight: 500) - .padding() - + .padding() } - } struct OnboardingButton: View { @@ -94,23 +123,28 @@ extension View { struct NewStepView: View { let title: LocalizedStringResource + let icon: Image let description: LocalizedStringResource let actions: Content - init(title: LocalizedStringResource, description: LocalizedStringResource, actions: () -> Content) { + init(title: LocalizedStringResource, description: LocalizedStringResource, systemImage: String, actions: () -> Content) { self.title = title + self.icon = Image(systemName: systemImage) self.description = description self.actions = actions() } var body: some View { - HStack { + HStack(spacing: 20) { + icon + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 24) VStack(alignment: .leading, spacing: 6) { Text(title) .bold() Text(description) } - Spacer(minLength: 20) actions } .padding(20) diff --git a/Sources/Secretive/Views/UpdateView.swift b/Sources/Secretive/Views/UpdateView.swift index 810e0e8..2540bda 100644 --- a/Sources/Secretive/Views/UpdateView.swift +++ b/Sources/Secretive/Views/UpdateView.swift @@ -12,7 +12,7 @@ struct UpdateDetailView: View { Text(.updateVersionName(updateName: update.name)).font(.title) GroupBox(label: Text(.updateReleaseNotesTitle)) { ScrollView { - attributedBody + Text(attributedBody) } } HStack { @@ -35,29 +35,62 @@ struct UpdateDetailView: View { .frame(maxWidth: 500) } - var attributedBody: Text { - var text = Text(verbatim: "") - for line in update.body.split(whereSeparator: \.isNewline) { - let attributed: Text - let split = line.split(separator: " ") - let unprefixed = split.dropFirst().joined(separator: " ") - if let prefix = split.first { - switch prefix { - case "#": - attributed = Text(unprefixed).font(.title) + Text(verbatim: "\n") - case "##": - attributed = Text(unprefixed).font(.title2) + Text(verbatim: "\n") - case "###": - attributed = Text(unprefixed).font(.title3) + Text(verbatim: "\n") + var attributedBody: AttributedString { + do { + var text = try AttributedString( + markdown: update.body, + options: .init( + allowsExtendedAttributes: true, + interpretedSyntax: .full, + ), + baseURL: URL(string: "https://github.com/maxgoedjen/secretive")! + ) + .transformingAttributes(AttributeScopes.FoundationAttributes.PresentationIntentAttribute.self) { key in + let font: Font? = switch key.value?.components.first?.kind { + case .header(level: 1): + Font.title + case .header(level: 2): + Font.title2 + case .header(level: 3): + Font.title3 default: - attributed = Text(line) + Text(verbatim: "\n\n") + nil + } + if let font { + key.replace(with: AttributeScopes.SwiftUIAttributes.FontAttribute.self, value: font) } - } else { - attributed = Text(line) + Text(verbatim: "\n\n") } - text = text + attributed + let lineBreak = AttributedString("\n\n") + for run in text.runs.reversed() { + text.insert(lineBreak, at: run.range.lowerBound) + } + return text + } catch { + var text = AttributedString() + for line in update.body.split(whereSeparator: \.isNewline) { + let attributed: AttributedString + let split = line.split(separator: " ") + let unprefixed = split.dropFirst().joined(separator: " ") + if let prefix = split.first { + var container = AttributeContainer() + switch prefix { + case "#": + container.font = .title + case "##": + container.font = .title2 + case "###": + container.font = .title3 + default: + continue + } + attributed = AttributedString(unprefixed, attributes: container) + } else { + attributed = AttributedString(line + "\n\n") + } + text = text + attributed + } + return text } - return text } } From b949d846c13717f475ab936e4b5bce64c7f3b008 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sat, 30 Aug 2025 18:56:52 -0700 Subject: [PATCH 03/17] WIP --- Sources/Packages/Localizable.xcstrings | 43 ++- Sources/Secretive.xcodeproj/project.pbxproj | 8 +- Sources/Secretive/App.swift | 2 +- .../Controllers/AgentStatusChecker.swift | 15 +- .../Controllers/LaunchAgentController.swift | 14 +- .../ShellConfigurationController.swift | 63 ---- .../PreviewAgentStatusChecker.swift | 5 +- .../Secretive/Views/ActionButtonStyle.swift | 27 ++ Sources/Secretive/Views/AgentStatusView.swift | 153 ++++++++ Sources/Secretive/Views/ContentView.swift | 54 +-- Sources/Secretive/Views/SetupView.swift | 354 ++++-------------- 11 files changed, 339 insertions(+), 399 deletions(-) delete mode 100644 Sources/Secretive/Controllers/ShellConfigurationController.swift create mode 100644 Sources/Secretive/Views/AgentStatusView.swift diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index 403b721..a3e0581 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -1,11 +1,19 @@ { "sourceLanguage" : "en", "strings" : { - ".zshrc" : { - - }, "Add Automatically" : { + }, + "agent_not_running_notice_detail_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "SecretAgent is a process that runs in the background to sign requests, so you don't need to keep Secretive open all the time.\n\n**Secretive will not be able to function properly unless the agent is installed and running.**" + } + } + } }, "agent_not_running_notice_title" : { "extractionState" : "manual", @@ -1169,6 +1177,9 @@ }, "Configure" : { + }, + "Copy" : { + }, "copyable_click_to_copy_button" : { "extractionState" : "manual", @@ -2489,6 +2500,9 @@ } } } + }, + "Disable Agent" : { + }, "Done" : { @@ -3406,6 +3420,18 @@ } } } + }, + "Restart Agent" : { + + }, + "Reveal in Finder" : { + + }, + "Running Since" : { + + }, + "Secret Agent Location" : { + }, "secret_detail_md5_fingerprint_label" : { "extractionState" : "manual", @@ -4335,9 +4361,6 @@ } } } - }, - "setup_ssh_add_to_config_button_%@" : { - }, "setup_ssh_added_manually_button" : { "extractionState" : "manual", @@ -5205,7 +5228,10 @@ } } }, - "TfileDialogMessageest" : { + "Socket Path" : { + + }, + "Start Agent" : { }, "unnamed_secret" : { @@ -6153,6 +6179,9 @@ } } } + }, + "Version" : { + } }, "version" : "1.0" diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index bf9352a..b09a304 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -36,7 +36,6 @@ 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; }; 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; }; 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; }; - 5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; }; 506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; }; 506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; }; 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; }; @@ -52,6 +51,7 @@ 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 */; }; + 50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB712E63BAF20072D2E7 /* AgentStatusView.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 */ @@ -121,7 +121,6 @@ 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = ""; }; 5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; }; 5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = ""; }; - 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = ""; }; 506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = ""; }; 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = ""; }; @@ -142,6 +141,7 @@ 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 = ""; }; + 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusView.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 */ @@ -256,6 +256,7 @@ 506772C82425BB8500034DED /* NoStoresView.swift */, 50153E1F250AFCB200525160 /* UpdateView.swift */, 5066A6C12516F303004B5A36 /* SetupView.swift */, + 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */, 50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */, 5066A6C72516FE6E004B5A36 /* CopyableView.swift */, ); @@ -269,7 +270,6 @@ 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, 50571E0424393D1500F76F6C /* LaunchAgentController.swift */, - 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */, ); path = Controllers; sourceTree = ""; @@ -445,8 +445,8 @@ 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */, - 5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */, 50033AC327813F1700253856 /* BundleIDs.swift in Sources */, + 50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */, 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, diff --git a/Sources/Secretive/App.swift b/Sources/Secretive/App.swift index 27ffeba..2e5653f 100644 --- a/Sources/Secretive/App.swift +++ b/Sources/Secretive/App.swift @@ -87,7 +87,7 @@ extension Secretive { private func reinstallAgent() { justUpdatedChecker.check() Task { - await LaunchAgentController().install() + _ = await LaunchAgentController().install() try? await Task.sleep(for: .seconds(1)) agentStatusChecker.check() if !agentStatusChecker.running { diff --git a/Sources/Secretive/Controllers/AgentStatusChecker.swift b/Sources/Secretive/Controllers/AgentStatusChecker.swift index 646cb4c..b7327a6 100644 --- a/Sources/Secretive/Controllers/AgentStatusChecker.swift +++ b/Sources/Secretive/Controllers/AgentStatusChecker.swift @@ -6,12 +6,14 @@ import Observation @MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable { var running: Bool { get } var developmentBuild: Bool { get } + var process: NSRunningApplication? { get } func check() } @Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol { var running: Bool = false + var process: NSRunningApplication? = nil nonisolated init() { Task { @MainActor in @@ -20,7 +22,8 @@ import Observation } func check() { - running = instanceSecretAgentProcess != nil + process = instanceSecretAgentProcess + running = process != nil } // All processes, including ones from older versions, etc @@ -34,7 +37,7 @@ import Observation let agents = allSecretAgentProcesses for agent in agents { guard let url = agent.bundleURL else { continue } - if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) { + if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) || (url.isXcodeURL && developmentBuild) { return agent } } @@ -43,9 +46,15 @@ import Observation // Whether Secretive is being run in an Xcode environment. var developmentBuild: Bool { - Bundle.main.bundleURL.absoluteString.contains("/Library/Developer/Xcode") + Bundle.main.bundleURL.isXcodeURL } } +extension URL { + var isXcodeURL: Bool { + absoluteString.contains("/Library/Developer/Xcode") + } + +} diff --git a/Sources/Secretive/Controllers/LaunchAgentController.swift b/Sources/Secretive/Controllers/LaunchAgentController.swift index ab0a912..c863d92 100644 --- a/Sources/Secretive/Controllers/LaunchAgentController.swift +++ b/Sources/Secretive/Controllers/LaunchAgentController.swift @@ -8,15 +8,23 @@ struct LaunchAgentController { private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController") - func install() async { + func install() async -> Bool { logger.debug("Installing agent") _ = setEnabled(false) // This is definitely a bit of a "seems to work better" thing but: // 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)) - await MainActor.run { - _ = setEnabled(true) + return await MainActor.run { + setEnabled(true) + } + } + + func uninstall() async -> Bool { + logger.debug("Uninstalling agent") + try? await Task.sleep(for: .seconds(1)) + return await MainActor.run { + setEnabled(false) } } diff --git a/Sources/Secretive/Controllers/ShellConfigurationController.swift b/Sources/Secretive/Controllers/ShellConfigurationController.swift deleted file mode 100644 index 2044160..0000000 --- a/Sources/Secretive/Controllers/ShellConfigurationController.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation -import Cocoa -import SecretKit - -struct ShellConfigurationController { - - let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String - - var shellInstructions: [ShellConfigInstruction] { - [ - ShellConfigInstruction(shell: "global", - shellConfigDirectory: "~/.ssh/", - shellConfigFilename: "config", - text: "Host *\n\tIdentityAgent \(socketPath)"), - ShellConfigInstruction(shell: "zsh", - shellConfigDirectory: "~/", - shellConfigFilename: ".zshrc", - text: "export SSH_AUTH_SOCK=\(socketPath)"), - ShellConfigInstruction(shell: "bash", - shellConfigDirectory: "~/", - shellConfigFilename: ".bashrc", - text: "export SSH_AUTH_SOCK=\(socketPath)"), - ShellConfigInstruction(shell: "fish", - shellConfigDirectory: "~/.config/fish", - shellConfigFilename: "config.fish", - text: "set -x SSH_AUTH_SOCK \(socketPath)"), - ] - - } - - - @MainActor func addToShell(shellInstructions: ShellConfigInstruction) -> Bool { - let openPanel = NSOpenPanel() - // This is sync, so no need to strongly retain - let delegate = Delegate(name: shellInstructions.shellConfigFilename) - openPanel.delegate = delegate - openPanel.message = "Select \(shellInstructions.shellConfigFilename) to let Secretive configure your shell automatically." - openPanel.prompt = "Add to \(shellInstructions.shellConfigFilename)" - openPanel.canChooseFiles = true - openPanel.canChooseDirectories = false - openPanel.showsHiddenFiles = true - openPanel.directoryURL = URL(fileURLWithPath: shellInstructions.shellConfigDirectory) - openPanel.nameFieldStringValue = shellInstructions.shellConfigFilename - openPanel.allowedContentTypes = [.symbolicLink, .data, .plainText] - openPanel.runModal() - guard let fileURL = openPanel.urls.first else { return false } - let handle: FileHandle - do { - handle = try FileHandle(forUpdating: fileURL) - guard let existing = try handle.readToEnd(), - let existingString = String(data: existing, encoding: .utf8) else { return false } - guard !existingString.contains(shellInstructions.text) else { - return true - } - try handle.seekToEnd() - } catch { - return false - } - handle.write(Data("\n# Secretive Config\n\(shellInstructions.text)\n".utf8)) - return true - } - -} diff --git a/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift b/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift index 51a5c09..e9799e9 100644 --- a/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift +++ b/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift @@ -1,12 +1,15 @@ import Foundation +import AppKit class PreviewAgentStatusChecker: AgentStatusCheckerProtocol { let running: Bool + let process: NSRunningApplication? let developmentBuild = false - init(running: Bool = true) { + init(running: Bool = true, process: NSRunningApplication? = nil) { self.running = running + self.process = process } func check() { diff --git a/Sources/Secretive/Views/ActionButtonStyle.swift b/Sources/Secretive/Views/ActionButtonStyle.swift index 4d7455f..cc0b4d6 100644 --- a/Sources/Secretive/Views/ActionButtonStyle.swift +++ b/Sources/Secretive/Views/ActionButtonStyle.swift @@ -22,3 +22,30 @@ extension View { } } + +struct DangerButtonModifier: ViewModifier { + + @Environment(\.colorScheme) var colorScheme + + func body(content: Content) -> some View { + // Tinted glass prominent is really hard to read on 26.0. + if #available(macOS 26.0, *), colorScheme == .dark { + content.buttonStyle(.glassProminent) + .tint(.red) + .foregroundStyle(.white) + } else { + content.buttonStyle(.borderedProminent) + .tint(.red) + .foregroundStyle(.white) + } + } + +} + +extension View { + + func danger() -> some View { + modifier(DangerButtonModifier()) + } + +} diff --git a/Sources/Secretive/Views/AgentStatusView.swift b/Sources/Secretive/Views/AgentStatusView.swift new file mode 100644 index 0000000..b829a15 --- /dev/null +++ b/Sources/Secretive/Views/AgentStatusView.swift @@ -0,0 +1,153 @@ +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 { + 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: "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: { + Task { + let controller = LaunchAgentController() + let installed = await controller.install() + if !installed { + _ = await controller.forceLaunch() + } + 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") { + Task { + let controller = LaunchAgentController() + let installed = await controller.install() + if !installed { + _ = await controller.forceLaunch() + } + agentStatusChecker.check() + } + } + .primary() + } + .padding(.vertical) + } + } + .formStyle(.grouped) + .frame(width: 400) + } + } + +} + +struct AgentInformationView: View { + + enum Action { + case copy + case revealInFinder + } + + let title: LocalizedStringResource + let value: String + let actions: Set + @State var tapping = false + + init(title: LocalizedStringResource, value: String, actions: Set = []) { + self.title = title + self.value = value + self.actions = actions + } + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text(title) + Spacer() + if actions.contains(.revealInFinder) { + Button("Reveal in Finder", systemImage: "folder") { + NSWorkspace.shared.selectFile(value, inFileViewerRootedAtPath: value) + } + .labelStyle(.iconOnly) + .buttonStyle(.borderless) + } + if actions.contains(.copy) { + Button("Copy", systemImage: "document.on.document") { + } + .labelStyle(.iconOnly) + .buttonStyle(.borderless) + } + } + Text(value) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } +} + +#Preview { + AgentStatusView() + .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false)) +} +#Preview { + AgentStatusView() + .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current)) +} diff --git a/Sources/Secretive/Views/ContentView.swift b/Sources/Secretive/Views/ContentView.swift index 899d12b..34e0405 100644 --- a/Sources/Secretive/Views/ContentView.swift +++ b/Sources/Secretive/Views/ContentView.swift @@ -56,7 +56,7 @@ extension ContentView { } var needsSetup: Bool { - (runningSetup || !hasRunSetup || !agentStatusChecker.running) && !agentStatusChecker.developmentBuild + runningSetup || !hasRunSetup } /// Item either showing a "everything's good, here's more info" or "something's wrong, re-run setup" message @@ -66,7 +66,7 @@ extension ContentView { if needsSetup { setupNoticeView } else { - runningNoticeView + agentStatusToolbarView } } @@ -125,43 +125,44 @@ extension ContentView { Button(action: { runningSetup = true }, label: { - Group { - if hasRunSetup && !agentStatusChecker.running { - Text(.agentNotRunningNoticeTitle) - } else { - Text(.agentSetupNoticeTitle) - } + if !hasRunSetup { + Text(.agentSetupNoticeTitle) + .font(.headline) } - .font(.headline) - }) .buttonStyle(ToolbarButtonStyle(color: .orange)) } @ViewBuilder - var runningNoticeView: some View { + var agentStatusToolbarView: some View { Button(action: { showingAgentInfo = true }, label: { HStack { - Text(.agentRunningNoticeTitle) - .font(.headline) - .foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white) - Circle() - .frame(width: 10, height: 10) - .foregroundColor(Color.green) + if agentStatusChecker.running { + Text(.agentRunningNoticeTitle) + .font(.headline) + .foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white) + Circle() + .frame(width: 10, height: 10) + .foregroundColor(Color.green) + } else { + Text(.agentNotRunningNoticeTitle) + .font(.headline) + Circle() + .frame(width: 10, height: 10) + .foregroundColor(Color.red) + } } }) - .buttonStyle(ToolbarButtonStyle(lightColor: .black.opacity(0.05), darkColor: .white.opacity(0.05))) + .buttonStyle( + ToolbarButtonStyle( + lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75), + darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5), + ) + ) .popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { - VStack { - Text(.agentRunningNoticeDetailTitle) - .font(.title) - .padding(5) - Text(.agentRunningNoticeDetailDescription) - .frame(width: 300) - } - .padding() + AgentStatusView() } } @@ -193,7 +194,6 @@ extension ContentView { } var attachmentAnchor: PopoverAttachmentAnchor { - // Ideally .point(.bottom), but broken on Sonoma (FB12726503) .rect(.bounds) } diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift index 030caf3..5c452a0 100644 --- a/Sources/Secretive/Views/SetupView.swift +++ b/Sources/Secretive/Views/SetupView.swift @@ -19,8 +19,7 @@ struct SetupView: View { ) { OnboardingButton("setup_agent_install_button", installed) { Task { - await LaunchAgentController().install() - installed = true + installed = await LaunchAgentController().install() } } } @@ -30,7 +29,7 @@ struct SetupView: View { description: "setup_updates_description", systemImage: "network.badge.shield.half.filled", ) { - OnboardingButton("setup_updates_ok", false) { + OnboardingButton("setup_updates_ok", updates) { Task { updates = true } @@ -43,27 +42,9 @@ struct SetupView: View { systemImage: "network.badge.shield.half.filled", ) { HStack { - OnboardingButton("setup_ssh_added_manually_button", false) { - sshConfig = true + OnboardingButton("Configure", false) { +// sshConfig = true } - OnboardingButton("Add Automatically", false) { - // let controller = ShellConfigurationController() - // if controller.addToShell(shellInstructions: selectedShellInstruction) { - // } - sshConfig = true - } - .fileImporter(isPresented: $sshConfig, allowedContentTypes: [.utf8PlainText, .symbolicLink, .data]) { result in - print(result) - } - // FIXME: - .fileDialogDefaultDirectory(URL(fileURLWithPath: "/Users/max/")) - .fileDialogBrowserOptions([.displayFileExtensions, .includeHiddenFiles]) - .fileExporterFilenameLabel(Text(".zshrc")) - .fileDialogMessage("TfileDialogMessageest") - .fileDialogConfirmationLabel("Configure") - .fileDialogURLEnabled(#Predicate { - return $0.lastPathComponent == ".zshrc" || $0.hasDirectoryPath - }) } } } @@ -152,232 +133,61 @@ struct NewStepView: View { } -struct OldSetupView: View { - - @State var stepIndex = 0 - @Binding var visible: Bool - @Binding var setupComplete: Bool - - var body: some View { - GeometryReader { proxy in - VStack { - StepView(numberOfSteps: 3, currentStep: stepIndex, width: proxy.size.width) - GeometryReader { _ in - HStack(spacing: 0) { - SecretAgentSetupView(buttonAction: advance) - .frame(width: proxy.size.width) - SSHAgentSetupView(buttonAction: advance) - .frame(width: proxy.size.width) - UpdaterExplainerView { - visible = false - setupComplete = true - } - .frame(width: proxy.size.width) - } - .offset(x: -proxy.size.width * Double(stepIndex), y: 0) - } - } - } - .frame(minWidth: 500, idealWidth: 500, minHeight: 500, idealHeight: 500) - } - - - func advance() { - withAnimation(.spring()) { - stepIndex += 1 - } - } - -} - -struct StepView: View { - - let numberOfSteps: Int - let currentStep: Int - - // Ideally we'd have a geometry reader inside this view doing this for us, but that crashes on 11.0b7 - let width: Double - - var body: some View { - ZStack(alignment: .leading) { - Rectangle() - .foregroundColor(.blue) - .frame(height: 5) - Rectangle() - .foregroundColor(.green) - .frame(width: max(0, ((width - (Constants.padding * 2)) / Double(numberOfSteps - 1)) * Double(currentStep) - (Constants.circleWidth / 2)), height: 5) - HStack { - ForEach(Array(0.. index { - Circle() - .foregroundColor(.green) - .frame(width: Constants.circleWidth, height: Constants.circleWidth) - Text("setup_step_complete_symbol") - .foregroundColor(.white) - .bold() - } else { - Circle() - .foregroundColor(.blue) - .frame(width: Constants.circleWidth, height: Constants.circleWidth) - if currentStep == index { - Circle() - .strokeBorder(Color.white, lineWidth: 3) - .frame(width: Constants.circleWidth, height: Constants.circleWidth) - } - Text(String(describing: index + 1)) - .foregroundColor(.white) - .bold() - } - } - if index < numberOfSteps - 1 { - Spacer(minLength: 30) - } - } - } - }.padding(Constants.padding) - } - -} - -extension StepView { - - enum Constants { - - static let padding: Double = 15 - static let circleWidth: Double = 30 - - } - -} - -struct SetupStepView : View where Content : View { - - let title: LocalizedStringKey - let image: Image - let bodyText: LocalizedStringKey - let buttonTitle: LocalizedStringKey - let buttonAction: () -> Void - let 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 - self.buttonTitle = buttonTitle - self.buttonAction = buttonAction - self.content = content() - } - - var body: some View { - VStack { - Text(title) - .font(.title) - Spacer() - image - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 64) - Spacer() - Text(bodyText) - .multilineTextAlignment(.center) - Spacer() - content - Spacer() - Button(buttonTitle) { - buttonAction() - } - }.padding() - } - -} - -struct SecretAgentSetupView: View { - - let buttonAction: () -> Void - - var body: some View { - SetupStepView(title: "setup_agent_title", - image: Image(nsImage: NSApplication.shared.applicationIconImage), - bodyText: "setup_agent_description", - buttonTitle: "setup_agent_install_button", - buttonAction: install) { - Text("setup_agent_activity_monitor_description") - .multilineTextAlignment(.center) - } - } - - func install() { - Task { - await LaunchAgentController().install() - buttonAction() - } - } - -} - -struct SSHAgentSetupView: View { - - let buttonAction: () -> Void - - private static let controller = ShellConfigurationController() - @State private var selectedShellInstruction: ShellConfigInstruction = controller.shellInstructions.first! - - var body: some View { - SetupStepView(title: "setup_ssh_title", - image: Image(systemName: "terminal"), - bodyText: "setup_ssh_description", - buttonTitle: "setup_ssh_added_manually_button", - buttonAction: buttonAction) { - 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) - .tag(instruction) - .padding() - } - }.pickerStyle(SegmentedPickerStyle()) - 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() - } - } - } - } - -} - -class Delegate: NSObject, NSOpenSavePanelDelegate { - - private let name: String - - init(name: String) { - self.name = name - } - - func panel(_ sender: Any, shouldEnable url: URL) -> Bool { - return url.lastPathComponent == name - } - -} - -struct UpdaterExplainerView: View { - - let buttonAction: () -> Void - - var body: some View { - SetupStepView(title: "setup_updates_title", - image: Image(systemName: "dot.radiowaves.left.and.right"), - bodyText: "setup_updates_description", - buttonTitle: "setup_updates_ok", - buttonAction: buttonAction) { - Link("setup_updates_readmore", destination: SetupView.Constants.updaterFAQURL) - } - } - -} +//struct SSHAgentSetupView: View { +// +// let buttonAction: () -> Void +// +// @State private var selectedShellInstruction: ShellConfigInstruction? +// +// private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String +// +// private var shellInstructions: [ShellConfigInstruction] { +// [ +// ShellConfigInstruction(shell: "global", +// shellConfigDirectory: "~/.ssh/", +// shellConfigFilename: "config", +// text: "Host *\n\tIdentityAgent \(socketPath)"), +// ShellConfigInstruction(shell: "zsh", +// shellConfigDirectory: "~/", +// shellConfigFilename: ".zshrc", +// text: "export SSH_AUTH_SOCK=\(socketPath)"), +// ShellConfigInstruction(shell: "bash", +// shellConfigDirectory: "~/", +// shellConfigFilename: ".bashrc", +// text: "export SSH_AUTH_SOCK=\(socketPath)"), +// ShellConfigInstruction(shell: "fish", +// shellConfigDirectory: "~/.config/fish", +// shellConfigFilename: "config.fish", +// text: "set -x SSH_AUTH_SOCK \(socketPath)"), +// ] +// +// } +// +// var body: some View { +// SetupStepView(title: "setup_ssh_title", +// image: Image(systemName: "terminal"), +// bodyText: "setup_ssh_description", +// buttonTitle: "setup_ssh_added_manually_button", +// buttonAction: buttonAction) { +// 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) +// .tag(instruction) +// .padding() +// } +// }.pickerStyle(SegmentedPickerStyle()) +// 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() +// } +// } +// } +// } +// +//} extension SetupView { @@ -404,46 +214,10 @@ struct ShellConfigInstruction: Identifiable, Hashable { } -#if DEBUG - -struct SetupView_Previews: PreviewProvider { - - static var previews: some View { - Group { - SetupView(visible: .constant(true), setupComplete: .constant(false)) - } - } - +#Preview { + SetupView(visible: .constant(true), setupComplete: .constant(false)) } -struct SecretAgentSetupView_Previews: PreviewProvider { - - static var previews: some View { - Group { - SecretAgentSetupView(buttonAction: {}) - } - } - -} - -struct SSHAgentSetupView_Previews: PreviewProvider { - - static var previews: some View { - Group { - SSHAgentSetupView(buttonAction: {}) - } - } - -} - -struct UpdaterExplainerView_Previews: PreviewProvider { - - static var previews: some View { - Group { - UpdaterExplainerView(buttonAction: {}) - } - } - -} - -#endif +//#Preview { +// SSHAgentSetupView(buttonAction: {}) +//} From cd76bb95ecfef9f92f3f923a01ebe4bf0abe42f0 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sun, 31 Aug 2025 00:58:16 -0700 Subject: [PATCH 04/17] 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()) + } + +} From fa658646d75e0d99d03c4a32df50fd390f091836 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sun, 31 Aug 2025 13:24:37 -0700 Subject: [PATCH 05/17] WIP --- Sources/Packages/Localizable.xcstrings | 16 ++- Sources/Secretive.xcodeproj/project.pbxproj | 4 + .../Secretive/Views/ActionButtonStyle.swift | 22 +++++ Sources/Secretive/Views/AgentStatusView.swift | 56 ++--------- .../Views/ConfigurationItemView.swift | 54 +++++++++++ .../Secretive/Views/ConfigurationView.swift | 97 ++++++++++++------- Sources/Secretive/Views/ContentView.swift | 7 +- .../Secretive/Views/SecretDetailView.swift | 16 +-- Sources/Secretive/Views/SetupView.swift | 56 ----------- 9 files changed, 173 insertions(+), 155 deletions(-) create mode 100644 Sources/Secretive/Views/ConfigurationItemView.swift diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index a3e0581..4e0650f 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -1,7 +1,10 @@ { "sourceLanguage" : "en", "strings" : { - "Add Automatically" : { + "" : { + + }, + "Add This:" : { }, "agent_not_running_notice_detail_description" : { @@ -1174,11 +1177,14 @@ } } } + }, + "Configuration File" : { + }, "Configure" : { }, - "Copy" : { + "Configuring" : { }, "copyable_click_to_copy_button" : { @@ -3858,6 +3864,9 @@ } } } + }, + "Secretive was unable to get SecretAgent to launch. Please try restarting your Mac, and if that doesn't work, file an issue on GitHub." : { + }, "secure_enclave" : { "extractionState" : "manual", @@ -5233,6 +5242,9 @@ }, "Start Agent" : { + }, + "Starting Agent" : { + }, "unnamed_secret" : { "extractionState" : "manual", diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index 8dd813b..65828b2 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 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 */; }; + 50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.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 */ @@ -144,6 +145,7 @@ 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 = ""; }; + 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.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 */ @@ -262,6 +264,7 @@ 50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */, 5066A6C72516FE6E004B5A36 /* CopyableView.swift */, 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */, + 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */, ); path = Views; sourceTree = ""; @@ -460,6 +463,7 @@ 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */, 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */, 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */, + 50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */, 50617D8323FCE48E0099B055 /* App.swift in Sources */, 506772C92425BB8500034DED /* NoStoresView.swift in Sources */, 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */, diff --git a/Sources/Secretive/Views/ActionButtonStyle.swift b/Sources/Secretive/Views/ActionButtonStyle.swift index cc0b4d6..1f6afdc 100644 --- a/Sources/Secretive/Views/ActionButtonStyle.swift +++ b/Sources/Secretive/Views/ActionButtonStyle.swift @@ -23,6 +23,28 @@ extension View { } +struct NormalButtonModifier: ViewModifier { + + func body(content: Content) -> some View { + if #available(macOS 26.0, *) { + content + .glassEffect(.regular.tint(.white.opacity(0.1)), in: .circle) + } else { + content + .buttonStyle(.borderless) + } + } + +} + +extension View { + + func normal() -> some View { + modifier(NormalButtonModifier()) + } + +} + struct DangerButtonModifier: ViewModifier { @Environment(\.colorScheme) var colorScheme diff --git a/Sources/Secretive/Views/AgentStatusView.swift b/Sources/Secretive/Views/AgentStatusView.swift index eab0ea0..6af8df5 100644 --- a/Sources/Secretive/Views/AgentStatusView.swift +++ b/Sources/Secretive/Views/AgentStatusView.swift @@ -21,22 +21,22 @@ struct AgentRunningView: View { Form { Section { if let process = agentStatusChecker.process { - AgentInformationView( + ConfigurationItemView( title: "Secret Agent Location", value: process.bundleURL!.path(), - actions: [.revealInFinder], + action: .revealInFinder(process.bundleURL!.path()), ) - AgentInformationView( + ConfigurationItemView( title: "Socket Path", value: socketPath, - actions: [.copy], + action: .copy(socketPath), ) - AgentInformationView( + ConfigurationItemView( title: "Version", value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String ) if let launchDate = process.launchDate { - AgentInformationView( + ConfigurationItemView( title: "Running Since", value: launchDate.formatted() ) @@ -144,50 +144,6 @@ struct AgentNotRunningView: View { } -struct AgentInformationView: View { - - enum Action { - case copy - case revealInFinder - } - - let title: LocalizedStringResource - let value: String - let actions: Set - @State var tapping = false - - init(title: LocalizedStringResource, value: String, actions: Set = []) { - self.title = title - self.value = value - self.actions = actions - } - - var body: some View { - VStack(alignment: .leading) { - HStack { - Text(title) - Spacer() - if actions.contains(.revealInFinder) { - Button("Reveal in Finder", systemImage: "folder") { - NSWorkspace.shared.selectFile(value, inFileViewerRootedAtPath: value) - } - .labelStyle(.iconOnly) - .buttonStyle(.borderless) - } - if actions.contains(.copy) { - Button("Copy", systemImage: "document.on.document") { - } - .labelStyle(.iconOnly) - .buttonStyle(.borderless) - } - } - Text(value) - .font(.subheadline) - .foregroundStyle(.secondary) - } - } -} - #Preview { AgentStatusView() .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false)) diff --git a/Sources/Secretive/Views/ConfigurationItemView.swift b/Sources/Secretive/Views/ConfigurationItemView.swift new file mode 100644 index 0000000..7f320e5 --- /dev/null +++ b/Sources/Secretive/Views/ConfigurationItemView.swift @@ -0,0 +1,54 @@ +import SwiftUI + +struct ConfigurationItemView: View { + + enum Action: Hashable { + case copy(String) + case revealInFinder(String) + } + + let title: LocalizedStringResource + let content: Content + let action: Action? + + init(title: LocalizedStringResource, value: String, action: Action? = nil) where Content == Text { + self.title = title + self.content = Text(value) + .font(.subheadline) + .foregroundStyle(.secondary) + self.action = action + } + + init(title: LocalizedStringResource, action: Action? = nil, content: () -> Content) { + self.title = title + self.content = content() + self.action = action + } + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text(title) + Spacer() + switch action { + case .copy(let string): + Button("Reveal in Finder", systemImage: "folder") { + NSWorkspace.shared.selectFile(string, inFileViewerRootedAtPath: string) + } + .labelStyle(.iconOnly) + .buttonStyle(.borderless) + case .revealInFinder(let string): + Button("Reveal in Finder", systemImage: "folder") { + NSWorkspace.shared.selectFile(string, inFileViewerRootedAtPath: string) + } + .labelStyle(.iconOnly) + .buttonStyle(.borderless) + case nil: + EmptyView() + } + } + content + } + } +} + diff --git a/Sources/Secretive/Views/ConfigurationView.swift b/Sources/Secretive/Views/ConfigurationView.swift index 6d9eef7..3db61e9 100644 --- a/Sources/Secretive/Views/ConfigurationView.swift +++ b/Sources/Secretive/Views/ConfigurationView.swift @@ -4,52 +4,79 @@ struct ConfigurationView: View { @Binding var visible: Bool - @State var running = true - @State var sshConfig = false - @Environment(\.agentStatusChecker) var agentStatusChecker + let buttonAction: () -> Void + + @State private var selectedShellInstruction: ShellConfigInstruction? + + private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String + + private var shellInstructions: [ShellConfigInstruction] { + [ + ShellConfigInstruction(shell: "SSH", + shellConfigDirectory: "~/.ssh/", + shellConfigFilename: "config", + text: "Host *\n\tIdentityAgent \(socketPath)"), + ShellConfigInstruction(shell: "zsh", + shellConfigDirectory: "~/", + shellConfigFilename: ".zshrc", + text: "export SSH_AUTH_SOCK=\(socketPath)"), + ShellConfigInstruction(shell: "bash", + shellConfigDirectory: "~/", + shellConfigFilename: ".bashrc", + text: "export SSH_AUTH_SOCK=\(socketPath)"), + ShellConfigInstruction(shell: "fish", + shellConfigDirectory: "~/.config/fish", + shellConfigFilename: "config.fish", + text: "set -x SSH_AUTH_SOCK \(socketPath)"), + ] + + } var body: some View { - VStack(spacing: 0) { - NewStepView( - title: "setup_agent_title", - description: "setup_agent_description", - systemImage: "network.badge.shield.half.filled", - ) { - OnboardingButton("setup_agent_install_button", running) { - Task { - _ = await LaunchAgentController().forceLaunch() - agentStatusChecker.check() - running = agentStatusChecker.running + Form { + Section { + Picker("Configuring", selection: $selectedShellInstruction) { + ForEach(shellInstructions) { instruction in + Text(instruction.shell) + .tag(instruction) + .padding() } } - } - Divider() - Divider() - NewStepView( - title: "setup_ssh_title", - description: "setup_ssh_description", - systemImage: "network.badge.shield.half.filled", - ) { - HStack { - OnboardingButton("setup_ssh_added_manually_button", false) { - sshConfig = true + if let selectedShellInstruction { + ConfigurationItemView(title: "Configuration File", value: selectedShellInstruction.shellConfigPath, action: .revealInFinder(selectedShellInstruction.shellConfigPath)) + ConfigurationItemView(title: "Add This:", action: .copy(selectedShellInstruction.text)) { + HStack { + Text(selectedShellInstruction.text) + .padding(8) + .font(.system(.subheadline, design: .monospaced)) + Spacer() + } + .frame(maxWidth: .infinity) + .background { + RoundedRectangle(cornerRadius: 6) + .fill(.black.opacity(0.05)) + .stroke(.separator, lineWidth: 1) + } + } - OnboardingButton("Add Automatically", false) { -// let controller = ShellConfigurationController() -// if controller.addToShell(shellInstructions: selectedShellInstruction) { -// } - sshConfig = true + Button("setup_ssh_add_for_me_button") { } } + } footer: { + Link("setup_third_party_faq_link", destination: URL(string: "https://github.com/maxgoedjen/secretive/blob/main/APP_CONFIG.md")!) } } - .background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10)) - .frame(minWidth: 500, idealWidth: 500, minHeight: 500, idealHeight: 500) - .padding() - .task { - running = agentStatusChecker.running + .formStyle(.grouped) + .onAppear { + selectedShellInstruction = shellInstructions.first } +// } } } + +#Preview { + ConfigurationView(visible: .constant(true)) {} + .frame(width: 400, height: 300) +} diff --git a/Sources/Secretive/Views/ContentView.swift b/Sources/Secretive/Views/ContentView.swift index 29d8a35..2e20ad5 100644 --- a/Sources/Secretive/Views/ContentView.swift +++ b/Sources/Secretive/Views/ContentView.swift @@ -103,11 +103,10 @@ extension ContentView { @ViewBuilder var newItemView: some View { if storeList.modifiableStore?.isAvailable ?? false { - Button(action: { + Button(.appMenuNewSecretButton, systemImage: "plus") { showingCreation = true - }, label: { - Image(systemName: "plus") - }) + } + .normal() .sheet(isPresented: $showingCreation) { if let modifiable = storeList.modifiableStore { CreateSecretView(store: modifiable, showing: $showingCreation) { created in diff --git a/Sources/Secretive/Views/SecretDetailView.swift b/Sources/Secretive/Views/SecretDetailView.swift index 140ee97..c727bc2 100644 --- a/Sources/Secretive/Views/SecretDetailView.swift +++ b/Sources/Secretive/Views/SecretDetailView.swift @@ -23,6 +23,12 @@ struct SecretDetailView: View { .frame(height: 20) CopyableView(title: .secretDetailPublicKeyPathLabel, image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.publicKeyPath(for: secret)) Spacer() + } header: { + Text(verbatim: secret.name) + .font(.system(size: 16, weight: .bold, design: .default)) + .foregroundStyle(.secondary) + .padding(.leading) + .padding(.bottom) } } .padding() @@ -45,12 +51,6 @@ extension URL { } -#if DEBUG - -struct SecretDetailView_Previews: PreviewProvider { - static var previews: some View { - SecretDetailView(secret: Preview.Store(numberOfRandomSecrets: 1).secrets[0]) - } +#Preview { + SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret")) } - -#endif diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift index 5c452a0..81d4ad2 100644 --- a/Sources/Secretive/Views/SetupView.swift +++ b/Sources/Secretive/Views/SetupView.swift @@ -133,62 +133,6 @@ struct NewStepView: View { } -//struct SSHAgentSetupView: View { -// -// let buttonAction: () -> Void -// -// @State private var selectedShellInstruction: ShellConfigInstruction? -// -// private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String -// -// private var shellInstructions: [ShellConfigInstruction] { -// [ -// ShellConfigInstruction(shell: "global", -// shellConfigDirectory: "~/.ssh/", -// shellConfigFilename: "config", -// text: "Host *\n\tIdentityAgent \(socketPath)"), -// ShellConfigInstruction(shell: "zsh", -// shellConfigDirectory: "~/", -// shellConfigFilename: ".zshrc", -// text: "export SSH_AUTH_SOCK=\(socketPath)"), -// ShellConfigInstruction(shell: "bash", -// shellConfigDirectory: "~/", -// shellConfigFilename: ".bashrc", -// text: "export SSH_AUTH_SOCK=\(socketPath)"), -// ShellConfigInstruction(shell: "fish", -// shellConfigDirectory: "~/.config/fish", -// shellConfigFilename: "config.fish", -// text: "set -x SSH_AUTH_SOCK \(socketPath)"), -// ] -// -// } -// -// var body: some View { -// SetupStepView(title: "setup_ssh_title", -// image: Image(systemName: "terminal"), -// bodyText: "setup_ssh_description", -// buttonTitle: "setup_ssh_added_manually_button", -// buttonAction: buttonAction) { -// 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) -// .tag(instruction) -// .padding() -// } -// }.pickerStyle(SegmentedPickerStyle()) -// 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() -// } -// } -// } -// } -// -//} - extension SetupView { enum Constants { From 9299bf343ffc29b5ca75efe30c788b9042d7123f Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Mon, 1 Sep 2025 14:52:17 -0700 Subject: [PATCH 06/17] WIP --- Sources/Packages/Localizable.xcstrings | 41 +++- Sources/Secretive.xcodeproj/project.pbxproj | 65 +++++- Sources/Secretive/App.swift | 15 +- .../Views/ConfigurationItemView.swift | 13 +- .../Secretive/Views/ConfigurationView.swift | 82 -------- Sources/Secretive/Views/ContentView.swift | 4 +- .../Secretive/Views/CreateSecretView.swift | 8 +- .../Secretive/Views/IntegrationsView.swift | 192 ++++++++++++++++++ .../Secretive/Views/SecretDetailView.swift | 6 - Sources/Secretive/Views/SetupView.swift | 32 +-- 10 files changed, 321 insertions(+), 137 deletions(-) delete mode 100644 Sources/Secretive/Views/ConfigurationView.swift create mode 100644 Sources/Secretive/Views/IntegrationsView.swift diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index 4e0650f..b7cb78c 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -787,6 +787,9 @@ } } } + }, + "Apps" : { + }, "auth_context_persist_for_duration" : { "comment" : "When the user clicks the notification to leave a secret unlocked, they are shown a prompt to approve the action. This is the description, showing which secret will used. The first placeholder is the name of the secret. The second placeholder is a localized description of the time period it will remain unlocked for (eg: \"five minutes\")", @@ -1184,7 +1187,7 @@ "Configure" : { }, - "Configuring" : { + "Copy" : { }, "copyable_click_to_copy_button" : { @@ -3039,6 +3042,15 @@ } } } + }, + "Getting Started" : { + + }, + "Integrations" : { + + }, + "Integrations..." : { + }, "no_secure_storage_description" : { "extractionState" : "manual", @@ -3270,6 +3282,12 @@ } } } + }, + "other" : { + + }, + "Other" : { + }, "persist_authentication_accept_button" : { "comment" : "When the user authorizes an action using a secret that requires unlock, they're shown a notification offering to leave the secret unlocked for a set period of time. This is the title for the notification.", @@ -5009,6 +5027,9 @@ } } } + }, + "Shell" : { + }, "signed_notification_description" : { "comment" : "When the user performs an action using a secret, they're shown a notification describing what happened. This is the description, showing which secret was used. The placeholder is the name of the secret.", @@ -5245,6 +5266,18 @@ }, "Starting Agent" : { + }, + "System" : { + + }, + "TBD" : { + + }, + "There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help." : { + + }, + "There's a community-maintained list of shell instructions on GitHub. If the shell you're looking for isn't supported, create an issue and the community may be able to help." : { + }, "unnamed_secret" : { "extractionState" : "manual", @@ -6194,6 +6227,12 @@ }, "Version" : { + }, + "View Documentation on Web" : { + + }, + "View on GitHub" : { + } }, "version" : "1.0" diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index 65828b2..029fe62 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -48,7 +48,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 */; }; + 50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */; }; 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 */; }; @@ -140,7 +140,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 = ""; }; + 50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationsView.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 = ""; }; 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusView.swift; sourceTree = ""; }; @@ -261,7 +261,7 @@ 50153E1F250AFCB200525160 /* UpdateView.swift */, 5066A6C12516F303004B5A36 /* SetupView.swift */, 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */, - 50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */, + 50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */, 5066A6C72516FE6E004B5A36 /* CopyableView.swift */, 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */, 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */, @@ -457,7 +457,7 @@ 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, - 50AE97002E5C1A420018C710 /* ConfigurationView.swift in Sources */, + 50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */, 50153E20250AFCB200525160 /* UpdateView.swift in Sources */, 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */, 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */, @@ -659,10 +659,18 @@ ENABLE_APP_SANDBOX = YES; ENABLE_ENHANCED_SECURITY = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_PREVIEWS = YES; - ENABLE_USER_SELECTED_FILES = readwrite; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; INFOPLIST_FILE = Secretive/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -691,10 +699,18 @@ ENABLE_APP_SANDBOX = YES; ENABLE_ENHANCED_SECURITY = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_PREVIEWS = YES; - ENABLE_USER_SELECTED_FILES = readwrite; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; INFOPLIST_FILE = Secretive/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -795,10 +811,18 @@ ENABLE_APP_SANDBOX = YES; ENABLE_ENHANCED_SECURITY = YES; ENABLE_HARDENED_RUNTIME = NO; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_POINTER_AUTHENTICATION = YES; ENABLE_PREVIEWS = YES; - ENABLE_USER_SELECTED_FILES = readwrite; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; INFOPLIST_FILE = Secretive/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -821,8 +845,17 @@ DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\""; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; INFOPLIST_FILE = SecretAgent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -847,8 +880,17 @@ DEVELOPMENT_TEAM = Z72PRUAWF6; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; INFOPLIST_FILE = SecretAgent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -874,8 +916,17 @@ DEVELOPMENT_TEAM = Z72PRUAWF6; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; INFOPLIST_FILE = SecretAgent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/Sources/Secretive/App.swift b/Sources/Secretive/App.swift index 2e5653f..bbd25c4 100644 --- a/Sources/Secretive/App.swift +++ b/Sources/Secretive/App.swift @@ -37,6 +37,7 @@ struct Secretive: App { @Environment(\.agentStatusChecker) var agentStatusChecker @AppStorage("defaultsHasRunSetup") var hasRunSetup = false @State private var showingSetup = false + @State private var showingIntegrations = false @State private var showingCreation = false @SceneBuilder var body: some Scene { @@ -58,8 +59,17 @@ struct Secretive: App { forceLaunchAgent() } } + .sheet(isPresented: $showingIntegrations) { + IntegrationsView() + .frame(minHeight: 400) + } } .commands { + CommandGroup(before: CommandGroupPlacement.appSettings) { + Button("Integrations...", systemImage: "app.connected.to.app.below.fill") { + showingIntegrations = true + } + } CommandGroup(after: CommandGroupPlacement.newItem) { Button(.appMenuNewSecretButton) { showingCreation = true @@ -71,11 +81,6 @@ struct Secretive: App { NSWorkspace.shared.open(Constants.helpURL) } } - CommandGroup(before: .help) { - Button(.appMenuSetupButton) { - showingSetup = true - } - } SidebarCommands() } } diff --git a/Sources/Secretive/Views/ConfigurationItemView.swift b/Sources/Secretive/Views/ConfigurationItemView.swift index 7f320e5..7544454 100644 --- a/Sources/Secretive/Views/ConfigurationItemView.swift +++ b/Sources/Secretive/Views/ConfigurationItemView.swift @@ -32,14 +32,19 @@ struct ConfigurationItemView: View { Spacer() switch action { case .copy(let string): - Button("Reveal in Finder", systemImage: "folder") { - NSWorkspace.shared.selectFile(string, inFileViewerRootedAtPath: string) + Button("Copy", systemImage: "document.on.document") { + NSPasteboard.general.declareTypes([.string], owner: nil) + NSPasteboard.general.setString(string, forType: .string) } .labelStyle(.iconOnly) .buttonStyle(.borderless) - case .revealInFinder(let string): + case .revealInFinder(let rawPath): Button("Reveal in Finder", systemImage: "folder") { - NSWorkspace.shared.selectFile(string, inFileViewerRootedAtPath: string) + // All foundation-based normalization methods replace this with the container directly. + let processedPath = rawPath.replacingOccurrences(of: "~", with: "/Users/\(NSUserName())") + let url = URL(filePath: processedPath) + let folder = url.deletingLastPathComponent().path() + NSWorkspace.shared.selectFile(processedPath, inFileViewerRootedAtPath: folder) } .labelStyle(.iconOnly) .buttonStyle(.borderless) diff --git a/Sources/Secretive/Views/ConfigurationView.swift b/Sources/Secretive/Views/ConfigurationView.swift deleted file mode 100644 index 3db61e9..0000000 --- a/Sources/Secretive/Views/ConfigurationView.swift +++ /dev/null @@ -1,82 +0,0 @@ -import SwiftUI - -struct ConfigurationView: View { - - @Binding var visible: Bool - - - let buttonAction: () -> Void - - @State private var selectedShellInstruction: ShellConfigInstruction? - - private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String - - private var shellInstructions: [ShellConfigInstruction] { - [ - ShellConfigInstruction(shell: "SSH", - shellConfigDirectory: "~/.ssh/", - shellConfigFilename: "config", - text: "Host *\n\tIdentityAgent \(socketPath)"), - ShellConfigInstruction(shell: "zsh", - shellConfigDirectory: "~/", - shellConfigFilename: ".zshrc", - text: "export SSH_AUTH_SOCK=\(socketPath)"), - ShellConfigInstruction(shell: "bash", - shellConfigDirectory: "~/", - shellConfigFilename: ".bashrc", - text: "export SSH_AUTH_SOCK=\(socketPath)"), - ShellConfigInstruction(shell: "fish", - shellConfigDirectory: "~/.config/fish", - shellConfigFilename: "config.fish", - text: "set -x SSH_AUTH_SOCK \(socketPath)"), - ] - - } - - var body: some View { - Form { - Section { - Picker("Configuring", selection: $selectedShellInstruction) { - ForEach(shellInstructions) { instruction in - Text(instruction.shell) - .tag(instruction) - .padding() - } - } - if let selectedShellInstruction { - ConfigurationItemView(title: "Configuration File", value: selectedShellInstruction.shellConfigPath, action: .revealInFinder(selectedShellInstruction.shellConfigPath)) - ConfigurationItemView(title: "Add This:", action: .copy(selectedShellInstruction.text)) { - HStack { - Text(selectedShellInstruction.text) - .padding(8) - .font(.system(.subheadline, design: .monospaced)) - Spacer() - } - .frame(maxWidth: .infinity) - .background { - RoundedRectangle(cornerRadius: 6) - .fill(.black.opacity(0.05)) - .stroke(.separator, lineWidth: 1) - } - - } - Button("setup_ssh_add_for_me_button") { - } - } - } footer: { - Link("setup_third_party_faq_link", destination: URL(string: "https://github.com/maxgoedjen/secretive/blob/main/APP_CONFIG.md")!) - } - } - .formStyle(.grouped) - .onAppear { - selectedShellInstruction = shellInstructions.first - } -// } - } - -} - -#Preview { - ConfigurationView(visible: .constant(true)) {} - .frame(width: 400, height: 300) -} diff --git a/Sources/Secretive/Views/ContentView.swift b/Sources/Secretive/Views/ContentView.swift index 2e20ad5..8ef73a7 100644 --- a/Sources/Secretive/Views/ContentView.swift +++ b/Sources/Secretive/Views/ContentView.swift @@ -36,7 +36,7 @@ struct ContentView: View { toolbarItem(newItemView, id: "new") } .sheet(isPresented: $runningSetup) { - SetupView(visible: $runningSetup, setupComplete: $hasRunSetup) + SetupView(setupComplete: $hasRunSetup) } } @@ -109,7 +109,7 @@ extension ContentView { .normal() .sheet(isPresented: $showingCreation) { if let modifiable = storeList.modifiableStore { - CreateSecretView(store: modifiable, showing: $showingCreation) { created in + CreateSecretView(store: modifiable) { created in if let created { activeSecret = created } diff --git a/Sources/Secretive/Views/CreateSecretView.swift b/Sources/Secretive/Views/CreateSecretView.swift index 9a5557d..f3e8b03 100644 --- a/Sources/Secretive/Views/CreateSecretView.swift +++ b/Sources/Secretive/Views/CreateSecretView.swift @@ -4,7 +4,7 @@ import SecretKit struct CreateSecretView: View { @State var store: StoreType - @Binding var showing: Bool + @Environment(\.dismiss) private var dismiss var createdSecret: (AnySecret?) -> Void @State private var name = "" @@ -109,7 +109,7 @@ struct CreateSecretView: View { .toggleStyle(.button) Spacer() Button(.createSecretCancelButton, role: .cancel) { - showing = false + dismiss() } Button(.createSecretCreateButton, action: save) .keyboardShortcut(.return) @@ -137,7 +137,7 @@ struct CreateSecretView: View { ) ) createdSecret(AnySecret(new)) - showing = false + dismiss() } catch { errorText = error.localizedDescription } @@ -147,5 +147,5 @@ struct CreateSecretView: View { } #Preview { - CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true)) { _ in } + CreateSecretView(store: Preview.StoreModifiable()) { _ in } } diff --git a/Sources/Secretive/Views/IntegrationsView.swift b/Sources/Secretive/Views/IntegrationsView.swift new file mode 100644 index 0000000..7e31674 --- /dev/null +++ b/Sources/Secretive/Views/IntegrationsView.swift @@ -0,0 +1,192 @@ +import SwiftUI + +struct IntegrationsView: View { + + @Environment(\.dismiss) private var dismiss + + @State private var selectedInstruction: ConfigurationFileInstructions? + + private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String + + private var instructions: [ConfigurationGroup] { + [ + ConfigurationGroup(name:"Integrations", instructions: [ + ConfigurationFileInstructions("Getting Started", id: .gettingStarted), + ]), + ConfigurationGroup(name: "System", instructions: [ + ConfigurationFileInstructions( + tool: "ssh", + configPath: "~/.ssh/config", + configText: "Host *\n\tIdentityAgent \(socketPath)", + website: URL(string: "https://man.openbsd.org/ssh_config.5")!, + ), + ConfigurationFileInstructions( + tool: "git", + configPath: "~/.gitconfig", + configText: "PLACEHOLDER", + website: URL(string: "https://git-scm.com/docs/git-config")! + ) + ]), + ConfigurationGroup(name: "Shell", instructions: [ + ConfigurationFileInstructions( + tool: "zsh", + configPath: "~/.zshrc", + configText: "export SSH_AUTH_SOCK=\(socketPath)" + ), + ConfigurationFileInstructions( + tool: "bash", + configPath: "~/.bashrc", + configText: "export SSH_AUTH_SOCK=\(socketPath)" + ), + ConfigurationFileInstructions( + tool: "fish", + configPath: "~/.config/fish/config.fish", + configText: "set -x SSH_AUTH_SOCK \(socketPath)" + ), + ConfigurationFileInstructions("other", id: .otherShell), + ]), + ConfigurationGroup(name:"Apps", instructions: [ + ConfigurationFileInstructions("Other", id: .otherApp), + ]), + ] + } + + var body: some View { + NavigationSplitView { + List(selection: $selectedInstruction) { + ForEach(instructions) { group in + Section(group.name) { + ForEach(group.instructions) { instruction in + Text(instruction.tool) + .padding(.vertical, 8) + .tag(instruction) + } + } + } + } + } detail: { + if let selectedInstruction { + Form { + switch selectedInstruction.id { + case .gettingStarted: + Text("TBD") + case .tool: + Section(selectedInstruction.tool) { + ConfigurationItemView(title: "Configuration File", value: selectedInstruction.configPath, action: .revealInFinder( selectedInstruction.configPath)) + ConfigurationItemView(title: "Add This:", action: .copy(selectedInstruction.configText)) { + HStack { + Text(selectedInstruction.configText) + .padding(8) + .font(.system(.subheadline, design: .monospaced)) + Spacer() + } + .frame(maxWidth: .infinity) + .background { + RoundedRectangle(cornerRadius: 6) + .fill(.black.opacity(0.05)) + .stroke(.separator, lineWidth: 1) + } + } + } + if let url = selectedInstruction.website { + Section { + Link(destination: url) { + VStack(alignment: .leading, spacing: 5) { + Text("View Documentation on Web") + .font(.headline) + Text(url.absoluteString) + .font(.caption2) + } + } + } + } + case .otherShell: + Section { + Link("View on GitHub", destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!) + } header: { + Text("There's a community-maintained list of shell instructions on GitHub. If the shell you're looking for isn't supported, create an issue and the community may be able to help.") + .font(.body) + } + case .otherApp: + Section { + Link("View on GitHub", destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!) + } header: { + Text("There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help.") + .font(.body) + } + } + } + .formStyle(.grouped) + } + } + .onAppear { + selectedInstruction = instructions.first?.instructions.first + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button("Done") { + dismiss() + } + .styled + } + } + } + +} + +struct ConfigurationGroup: Identifiable { + let id = UUID() + var name: LocalizedStringResource + var instructions: [ConfigurationFileInstructions] = [] +} + +struct ConfigurationFileInstructions: Identifiable, Hashable { + + var id: ID + var tool: String + var configPath: String + var configText: String + var website: URL? + + init(tool: String, configPath: String, configText: String, website: URL? = nil) { + self.id = .tool(tool) + self.tool = tool + self.configPath = configPath + self.configText = configText + self.website = website + } + + init(_ name: LocalizedStringResource, id: ID) { + self.id = id + tool = String(localized: name) + configPath = "" + configText = "" + } + + enum ID: Identifiable, Hashable { + case gettingStarted + case tool(String) + case otherShell + case otherApp + + var id: String { + switch self { + case .gettingStarted: + "getting_started" + case .tool(let name): + name + case .otherShell: + "other_shell" + case .otherApp: + "other_app" + } + } + } + +} + + +#Preview { + IntegrationsView() + .frame(height: 500) +} diff --git a/Sources/Secretive/Views/SecretDetailView.swift b/Sources/Secretive/Views/SecretDetailView.swift index c727bc2..1e616f1 100644 --- a/Sources/Secretive/Views/SecretDetailView.swift +++ b/Sources/Secretive/Views/SecretDetailView.swift @@ -23,12 +23,6 @@ struct SecretDetailView: View { .frame(height: 20) CopyableView(title: .secretDetailPublicKeyPathLabel, image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.publicKeyPath(for: secret)) Spacer() - } header: { - Text(verbatim: secret.name) - .font(.system(size: 16, weight: .bold, design: .default)) - .foregroundStyle(.secondary) - .padding(.leading) - .padding(.bottom) } } .padding() diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift index 81d4ad2..6aefefc 100644 --- a/Sources/Secretive/Views/SetupView.swift +++ b/Sources/Secretive/Views/SetupView.swift @@ -2,7 +2,7 @@ import SwiftUI struct SetupView: View { - @Binding var visible: Bool + @Environment(\.dismiss) private var dismiss @Binding var setupComplete: Bool @State var installed = false @@ -12,7 +12,7 @@ struct SetupView: View { var body: some View { VStack { VStack(spacing: 0) { - NewStepView( + StepView( title: "setup_agent_title", description: "setup_agent_description", systemImage: "lock.laptopcomputer", @@ -24,7 +24,7 @@ struct SetupView: View { } } Divider() - NewStepView( + StepView( title: "setup_updates_title", description: "setup_updates_description", systemImage: "network.badge.shield.half.filled", @@ -36,7 +36,7 @@ struct SetupView: View { } } Divider() - NewStepView( + StepView( title: "setup_ssh_title", description: "setup_ssh_description", systemImage: "network.badge.shield.half.filled", @@ -101,7 +101,7 @@ extension View { } -struct NewStepView: View { +struct StepView: View { let title: LocalizedStringResource let icon: Image @@ -141,27 +141,7 @@ extension SetupView { } -struct ShellConfigInstruction: Identifiable, Hashable { - - var shell: String - var shellConfigDirectory: String - var shellConfigFilename: String - var text: String - - var id: String { - shell - } - - var shellConfigPath: String { - return (shellConfigDirectory as NSString).appendingPathComponent(shellConfigFilename) - } - -} #Preview { - SetupView(visible: .constant(true), setupComplete: .constant(false)) + SetupView(setupComplete: .constant(false)) } - -//#Preview { -// SSHAgentSetupView(buttonAction: {}) -//} From c8d90ba455e8ad13275c508b209d87e7876ae691 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Mon, 1 Sep 2025 15:09:27 -0700 Subject: [PATCH 07/17] WIP --- .../OpenSSH/OpenSSHCertificateHandler.swift | 2 +- .../PublicKeyStandinFileController.swift | 16 ++--- Sources/SecretAgent/AppDelegate.swift | 2 +- .../Secretive/Views/IntegrationsView.swift | 60 ++++++++++++------- .../Secretive/Views/SecretDetailView.swift | 6 +- Sources/Secretive/Views/SetupView.swift | 1 - 6 files changed, 51 insertions(+), 36 deletions(-) diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift index 23d64ce..ac4ced2 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift @@ -4,7 +4,7 @@ import OSLog /// Manages storage and lookup for OpenSSH certificates. public actor OpenSSHCertificateHandler: Sendable { - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) + private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory) private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler") private let writer = OpenSSHPublicKeyWriter() private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:] diff --git a/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift b/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift index ada02d7..71bb426 100644 --- a/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift +++ b/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift @@ -5,12 +5,12 @@ import OSLog public final class PublicKeyFileStoreController: Sendable { private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController") - private let directory: String + private let directory: URL private let keyWriter = OpenSSHPublicKeyWriter() /// Initializes a PublicKeyFileStoreController. - public init(homeDirectory: String) { - directory = homeDirectory.appending("/PublicKeys") + public init(homeDirectory: URL) { + directory = homeDirectory.appending(component: "PublicKeys") } /// Writes out the keys specified to disk. @@ -20,7 +20,7 @@ public final class PublicKeyFileStoreController: Sendable { logger.log("Writing public keys to disk") if clear { let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) })) - let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory)) ?? [] + let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? [] let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" } let untracked = Set(fullPathContents) @@ -29,7 +29,7 @@ public final class PublicKeyFileStoreController: Sendable { try? FileManager.default.removeItem(at: URL(fileURLWithPath: path)) } } - try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil) + try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil) for secret in secrets { let path = publicKeyPath(for: secret) let data = Data(keyWriter.openSSHString(secret: secret).utf8) @@ -44,14 +44,14 @@ public final class PublicKeyFileStoreController: Sendable { /// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to. public func publicKeyPath(for secret: SecretType) -> String { let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") - return directory.appending("/").appending("\(minimalHex).pub") + return directory.appending(component: "\(minimalHex).pub").path() } /// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory. public var hasAnyCertificates: Bool { do { return try FileManager.default - .contentsOfDirectory(atPath: directory) + .contentsOfDirectory(atPath: directory.path()) .filter { $0.hasSuffix("-cert.pub") } .isEmpty == false } catch { @@ -65,7 +65,7 @@ public final class PublicKeyFileStoreController: Sendable { /// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be. public func sshCertificatePath(for secret: SecretType) -> String { let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") - return directory.appending("/").appending("\(minimalHex)-cert.pub") + return directory.appending(component: "\(minimalHex)-cert.pub").path() } } diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index 5800c75..877145e 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -21,7 +21,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { }() private let updater = Updater(checkOnLaunch: true) private let notifier = Notifier() - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) + private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory) private lazy var agent: Agent = { Agent(storeList: storeList, witness: notifier) }() diff --git a/Sources/Secretive/Views/IntegrationsView.swift b/Sources/Secretive/Views/IntegrationsView.swift index 7e31674..e91c63b 100644 --- a/Sources/Secretive/Views/IntegrationsView.swift +++ b/Sources/Secretive/Views/IntegrationsView.swift @@ -71,20 +71,24 @@ struct IntegrationsView: View { case .gettingStarted: Text("TBD") case .tool: - Section(selectedInstruction.tool) { - ConfigurationItemView(title: "Configuration File", value: selectedInstruction.configPath, action: .revealInFinder( selectedInstruction.configPath)) - ConfigurationItemView(title: "Add This:", action: .copy(selectedInstruction.configText)) { - HStack { - Text(selectedInstruction.configText) - .padding(8) - .font(.system(.subheadline, design: .monospaced)) - Spacer() - } - .frame(maxWidth: .infinity) - .background { - RoundedRectangle(cornerRadius: 6) - .fill(.black.opacity(0.05)) - .stroke(.separator, lineWidth: 1) + ForEach(selectedInstruction.steps) { stepGroup in + Section { + ConfigurationItemView(title: "Configuration File", value: stepGroup.path, action: .revealInFinder(stepGroup.path)) + ForEach(stepGroup.steps, id: \.self) { step in + ConfigurationItemView(title: "Add This:", action: .copy(step)) { + HStack { + Text(step) + .padding(8) + .font(.system(.subheadline, design: .monospaced)) + Spacer() + } + .frame(maxWidth: .infinity) + .background { + RoundedRectangle(cornerRadius: 6) + .fill(.black.opacity(0.05)) + .stroke(.separator, lineWidth: 1) + } + } } } } @@ -140,27 +144,39 @@ struct ConfigurationGroup: Identifiable { var instructions: [ConfigurationFileInstructions] = [] } -struct ConfigurationFileInstructions: Identifiable, Hashable { +struct ConfigurationFileInstructions: Hashable, Identifiable { + + struct StepGroup: Hashable, Identifiable { + let path: String + let steps: [String] + var id: String { path } + } var id: ID var tool: String - var configPath: String - var configText: String + var steps: [StepGroup] var website: URL? + var note: String? - init(tool: String, configPath: String, configText: String, website: URL? = nil) { + init(tool: String, configPath: String, configText: String, website: URL? = nil, note: String? = nil) { self.id = .tool(tool) self.tool = tool - self.configPath = configPath - self.configText = configText + self.steps = [StepGroup(path: configPath, steps: [configText])] + self.website = website + self.note = note + } + + init(tool: String, steps: [StepGroup], website: URL? = nil, note: String? = nil) { + self.id = .tool(tool) + self.tool = tool + self.steps = steps self.website = website } init(_ name: LocalizedStringResource, id: ID) { self.id = id tool = String(localized: name) - configPath = "" - configText = "" + self.steps = [] } enum ID: Identifiable, Hashable { diff --git a/Sources/Secretive/Views/SecretDetailView.swift b/Sources/Secretive/Views/SecretDetailView.swift index 1e616f1..69b6d33 100644 --- a/Sources/Secretive/Views/SecretDetailView.swift +++ b/Sources/Secretive/Views/SecretDetailView.swift @@ -6,7 +6,7 @@ struct SecretDetailView: View { let secret: SecretType private let keyWriter = OpenSSHPublicKeyWriter() - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.agentHomePath) + private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL) var body: some View { ScrollView { @@ -39,8 +39,8 @@ struct SecretDetailView: View { extension URL { - static var agentHomePath: String { - URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) + static var agentHomeURL: URL { + URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID)) } } diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift index 6aefefc..eaa1652 100644 --- a/Sources/Secretive/Views/SetupView.swift +++ b/Sources/Secretive/Views/SetupView.swift @@ -141,7 +141,6 @@ extension SetupView { } - #Preview { SetupView(setupComplete: .constant(false)) } From 2d05a7b0f30c5d3b635cb83d8c8ea9db67deed35 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Mon, 1 Sep 2025 15:22:52 -0700 Subject: [PATCH 08/17] WIP --- .../Secretive/Views/IntegrationsView.swift | 133 +++++++++++------- 1 file changed, 86 insertions(+), 47 deletions(-) diff --git a/Sources/Secretive/Views/IntegrationsView.swift b/Sources/Secretive/Views/IntegrationsView.swift index e91c63b..13b1e7a 100644 --- a/Sources/Secretive/Views/IntegrationsView.swift +++ b/Sources/Secretive/Views/IntegrationsView.swift @@ -8,49 +8,6 @@ struct IntegrationsView: View { private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String - private var instructions: [ConfigurationGroup] { - [ - ConfigurationGroup(name:"Integrations", instructions: [ - ConfigurationFileInstructions("Getting Started", id: .gettingStarted), - ]), - ConfigurationGroup(name: "System", instructions: [ - ConfigurationFileInstructions( - tool: "ssh", - configPath: "~/.ssh/config", - configText: "Host *\n\tIdentityAgent \(socketPath)", - website: URL(string: "https://man.openbsd.org/ssh_config.5")!, - ), - ConfigurationFileInstructions( - tool: "git", - configPath: "~/.gitconfig", - configText: "PLACEHOLDER", - website: URL(string: "https://git-scm.com/docs/git-config")! - ) - ]), - ConfigurationGroup(name: "Shell", instructions: [ - ConfigurationFileInstructions( - tool: "zsh", - configPath: "~/.zshrc", - configText: "export SSH_AUTH_SOCK=\(socketPath)" - ), - ConfigurationFileInstructions( - tool: "bash", - configPath: "~/.bashrc", - configText: "export SSH_AUTH_SOCK=\(socketPath)" - ), - ConfigurationFileInstructions( - tool: "fish", - configPath: "~/.config/fish/config.fish", - configText: "set -x SSH_AUTH_SOCK \(socketPath)" - ), - ConfigurationFileInstructions("other", id: .otherShell), - ]), - ConfigurationGroup(name:"Apps", instructions: [ - ConfigurationFileInstructions("Other", id: .otherApp), - ]), - ] - } - var body: some View { NavigationSplitView { List(selection: $selectedInstruction) { @@ -90,6 +47,11 @@ struct IntegrationsView: View { } } } + } footer: { + if let note = stepGroup.note { + Text(note) + .font(.caption) + } } } if let url = selectedInstruction.website { @@ -138,6 +100,78 @@ struct IntegrationsView: View { } +extension IntegrationsView { + + fileprivate var instructions: [ConfigurationGroup] { + [ + ConfigurationGroup(name:"Integrations", instructions: [ + ConfigurationFileInstructions("Getting Started", id: .gettingStarted), + ]), + ConfigurationGroup( + name: "System", + instructions: [ + ConfigurationFileInstructions( + tool: "SSH", + configPath: "~/.ssh/config", + configText: "Host *\n\tIdentityAgent \(socketPath)", + website: URL(string: "https://man.openbsd.org/ssh_config.5")!, + ), + ConfigurationFileInstructions( + tool: "Git Signing", + steps: [ + .init(path: "~/.gitconfig", steps: [ + """ + [user] + signingkey = YOUR_PUBLIC_KEY_PATH + [commit] + gpgsign = true + [gpg] + format = ssh + [gpg "ssh"] + allowedSignersFile = ~/.gitallowedsigners + """ + ], + note: "If any section (like [user]) already exists, just add the entries in the existing section." + + ), + .init( + path: "~/.gitallowedsigners", + steps: [ + "YOUR_PUBLIC_KEY" + ], + note: "~/.gitallowedsigners probably does not exist. You'll need to create it." + ), + ], + website: URL(string: "https://git-scm.com/docs/git-config")!, + ) + ] + ), + ConfigurationGroup(name: "Shell", instructions: [ + ConfigurationFileInstructions( + tool: "zsh", + configPath: "~/.zshrc", + configText: "export SSH_AUTH_SOCK=\(socketPath)" + ), + ConfigurationFileInstructions( + tool: "bash", + configPath: "~/.bashrc", + configText: "export SSH_AUTH_SOCK=\(socketPath)" + ), + ConfigurationFileInstructions( + tool: "fish", + configPath: "~/.config/fish/config.fish", + configText: "set -x SSH_AUTH_SOCK \(socketPath)" + ), + ConfigurationFileInstructions("other", id: .otherShell), + ]), + ConfigurationGroup(name:"Apps", instructions: [ + ConfigurationFileInstructions("Other", id: .otherApp), + ]), + ] + } + +} + struct ConfigurationGroup: Identifiable { let id = UUID() var name: LocalizedStringResource @@ -149,24 +183,29 @@ struct ConfigurationFileInstructions: Hashable, Identifiable { struct StepGroup: Hashable, Identifiable { let path: String let steps: [String] + let note: String? var id: String { path } + + init(path: String, steps: [String], note: String? = nil) { + self.path = path + self.steps = steps + self.note = note + } } var id: ID var tool: String var steps: [StepGroup] var website: URL? - var note: String? - init(tool: String, configPath: String, configText: String, website: URL? = nil, note: String? = nil) { + init(tool: String, configPath: String, configText: String, website: URL? = nil) { self.id = .tool(tool) self.tool = tool self.steps = [StepGroup(path: configPath, steps: [configText])] self.website = website - self.note = note } - init(tool: String, steps: [StepGroup], website: URL? = nil, note: String? = nil) { + init(tool: String, steps: [StepGroup], website: URL? = nil) { self.id = .tool(tool) self.tool = tool self.steps = steps From 4d84621b3d60f81f477db1a4206acf9e773e9940 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Mon, 1 Sep 2025 16:10:27 -0700 Subject: [PATCH 09/17] WIP --- Sources/Packages/Localizable.xcstrings | 27 +- .../Secretive/Views/IntegrationsView.swift | 238 +++++++++++------- 2 files changed, 166 insertions(+), 99 deletions(-) diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index b7cb78c..ed18ceb 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -1186,6 +1186,9 @@ }, "Configure" : { + }, + "Configuring Tools for Secretive" : { + }, "Copy" : { @@ -3045,12 +3048,27 @@ }, "Getting Started" : { + }, + "If you don't known what shell you use and haven't changed it, you're probably using `zsh`." : { + + }, + "If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client." : { + + }, + "If you're trying to configure anything your command line runs to use Secretive, configure your shell." : { + + }, + "If you're trying to sign your git commits, set up Git Signing." : { + }, "Integrations" : { }, "Integrations..." : { + }, + "Most tools will try and look for SSH keys on disk in `~/.ssh`. To use Secretive, we need to configure those tools to talk to Secretive instead." : { + }, "no_secure_storage_description" : { "extractionState" : "manual", @@ -5269,9 +5287,6 @@ }, "System" : { - }, - "TBD" : { - }, "There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help." : { @@ -6233,6 +6248,12 @@ }, "View on GitHub" : { + }, + "What Should I Configure?" : { + + }, + "You can configure more than one tool, they generally won't interfere with each other." : { + } }, "version" : "1.0" diff --git a/Sources/Secretive/Views/IntegrationsView.swift b/Sources/Secretive/Views/IntegrationsView.swift index 13b1e7a..7cef900 100644 --- a/Sources/Secretive/Views/IntegrationsView.swift +++ b/Sources/Secretive/Views/IntegrationsView.swift @@ -23,66 +23,99 @@ struct IntegrationsView: View { } } detail: { if let selectedInstruction { - Form { - switch selectedInstruction.id { - case .gettingStarted: - Text("TBD") - case .tool: - ForEach(selectedInstruction.steps) { stepGroup in - Section { - ConfigurationItemView(title: "Configuration File", value: stepGroup.path, action: .revealInFinder(stepGroup.path)) - ForEach(stepGroup.steps, id: \.self) { step in - ConfigurationItemView(title: "Add This:", action: .copy(step)) { - HStack { - Text(step) - .padding(8) - .font(.system(.subheadline, design: .monospaced)) - Spacer() - } - .frame(maxWidth: .infinity) - .background { - RoundedRectangle(cornerRadius: 6) - .fill(.black.opacity(0.05)) - .stroke(.separator, lineWidth: 1) - } - } - } - } footer: { - if let note = stepGroup.note { - Text(note) - .font(.caption) + switch selectedInstruction.id { + case .gettingStarted: + Form { + Section("Configuring Tools for Secretive") { + Text("Most tools will try and look for SSH keys on disk in `~/.ssh`. To use Secretive, we need to configure those tools to talk to Secretive instead.") + } + Section { + NavigationLink(value: ssh) { + Text("If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client.") + } + NavigationLink(value: zsh) { + VStack(alignment: .leading) { + Text("If you're trying to configure anything your command line runs to use Secretive, configure your shell.") + Text("If you don't known what shell you use and haven't changed it, you're probably using `zsh`.") + .font(.caption2) + .foregroundStyle(.secondary) } } - } - if let url = selectedInstruction.website { - Section { - Link(destination: url) { - VStack(alignment: .leading, spacing: 5) { - Text("View Documentation on Web") - .font(.headline) - Text(url.absoluteString) - .font(.caption2) - } - } + NavigationLink(value: git) { + Text("If you're trying to sign your git commits, set up Git Signing.") } - } - case .otherShell: - Section { - Link("View on GitHub", destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!) } header: { - Text("There's a community-maintained list of shell instructions on GitHub. If the shell you're looking for isn't supported, create an issue and the community may be able to help.") - .font(.body) + Text("What Should I Configure?") } - case .otherApp: - Section { - Link("View on GitHub", destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!) - } header: { - Text("There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help.") - .font(.body) + footer: { + Text("You can configure more than one tool, they generally won't interfere with each other.") } } - } - .formStyle(.grouped) + .formStyle(.grouped) + case .tool: + Form { + ForEach(selectedInstruction.steps) { stepGroup in + Section { + ConfigurationItemView(title: "Configuration File", value: stepGroup.path, action: .revealInFinder(stepGroup.path)) + ForEach(stepGroup.steps, id: \.self) { step in + ConfigurationItemView(title: "Add This:", action: .copy(step)) { + HStack { + Text(step) + .padding(8) + .font(.system(.subheadline, design: .monospaced)) + Spacer() + } + .frame(maxWidth: .infinity) + .background { + RoundedRectangle(cornerRadius: 6) + .fill(.black.opacity(0.05)) + .stroke(.separator, lineWidth: 1) + } + } + } + } footer: { + if let note = stepGroup.note { + Text(note) + .font(.caption) + } + } + } + if let url = selectedInstruction.website { + Section { + Link(destination: url) { + VStack(alignment: .leading, spacing: 5) { + Text("View Documentation on Web") + .font(.headline) + Text(url.absoluteString) + .font(.caption2) + } + } + } + } + } + .formStyle(.grouped) + case .otherShell: + Form { + Section { + Link("View on GitHub", destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!) + } header: { + Text("There's a community-maintained list of shell instructions on GitHub. If the shell you're looking for isn't supported, create an issue and the community may be able to help.") + .font(.body) + } + } + .formStyle(.grouped) + + case .otherApp: + Form { + Section { + Link("View on GitHub", destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!) + } header: { + Text("There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help.") + .font(.body) + } + } + .formStyle(.grouped) + } } } .onAppear { @@ -102,6 +135,55 @@ struct IntegrationsView: View { extension IntegrationsView { + fileprivate var ssh: ConfigurationFileInstructions { + ConfigurationFileInstructions( + tool: "SSH", + configPath: "~/.ssh/config", + configText: "Host *\n\tIdentityAgent \(socketPath)", + website: URL(string: "https://man.openbsd.org/ssh_config.5")!, + note: "You can tell SSH to use a specific key for a given host. See the web documentation for more details.", + ) + } + + fileprivate var git: ConfigurationFileInstructions { + ConfigurationFileInstructions( + tool: "Git Signing", + steps: [ + .init(path: "~/.gitconfig", steps: [ + """ + [user] + signingkey = YOUR_PUBLIC_KEY_PATH + [commit] + gpgsign = true + [gpg] + format = ssh + [gpg "ssh"] + allowedSignersFile = ~/.gitallowedsigners + """ + ], + note: "If any section (like [user]) already exists, just add the entries in the existing section." + + ), + .init( + path: "~/.gitallowedsigners", + steps: [ + "YOUR_PUBLIC_KEY" + ], + note: "~/.gitallowedsigners probably does not exist. You'll need to create it." + ), + ], + website: URL(string: "https://git-scm.com/docs/git-config")!, + ) + } + + fileprivate var zsh: ConfigurationFileInstructions { + ConfigurationFileInstructions( + tool: "zsh", + configPath: "~/.zshrc", + configText: "export SSH_AUTH_SOCK=\(socketPath)" + ) + } + fileprivate var instructions: [ConfigurationGroup] { [ ConfigurationGroup(name:"Integrations", instructions: [ @@ -110,48 +192,12 @@ extension IntegrationsView { ConfigurationGroup( name: "System", instructions: [ - ConfigurationFileInstructions( - tool: "SSH", - configPath: "~/.ssh/config", - configText: "Host *\n\tIdentityAgent \(socketPath)", - website: URL(string: "https://man.openbsd.org/ssh_config.5")!, - ), - ConfigurationFileInstructions( - tool: "Git Signing", - steps: [ - .init(path: "~/.gitconfig", steps: [ - """ - [user] - signingkey = YOUR_PUBLIC_KEY_PATH - [commit] - gpgsign = true - [gpg] - format = ssh - [gpg "ssh"] - allowedSignersFile = ~/.gitallowedsigners - """ - ], - note: "If any section (like [user]) already exists, just add the entries in the existing section." - - ), - .init( - path: "~/.gitallowedsigners", - steps: [ - "YOUR_PUBLIC_KEY" - ], - note: "~/.gitallowedsigners probably does not exist. You'll need to create it." - ), - ], - website: URL(string: "https://git-scm.com/docs/git-config")!, - ) + ssh, + git, ] ), ConfigurationGroup(name: "Shell", instructions: [ - ConfigurationFileInstructions( - tool: "zsh", - configPath: "~/.zshrc", - configText: "export SSH_AUTH_SOCK=\(socketPath)" - ), + zsh, ConfigurationFileInstructions( tool: "bash", configPath: "~/.bashrc", @@ -164,8 +210,8 @@ extension IntegrationsView { ), ConfigurationFileInstructions("other", id: .otherShell), ]), - ConfigurationGroup(name:"Apps", instructions: [ - ConfigurationFileInstructions("Other", id: .otherApp), + ConfigurationGroup(name: "Other", instructions: [ + ConfigurationFileInstructions("Apps", id: .otherApp), ]), ] } @@ -198,10 +244,10 @@ struct ConfigurationFileInstructions: Hashable, Identifiable { var steps: [StepGroup] var website: URL? - init(tool: String, configPath: String, configText: String, website: URL? = nil) { + init(tool: String, configPath: String, configText: String, website: URL? = nil, note: String? = nil) { self.id = .tool(tool) self.tool = tool - self.steps = [StepGroup(path: configPath, steps: [configText])] + self.steps = [StepGroup(path: configPath, steps: [configText], note: note)] self.website = website } From ea96dd88eb950f763783aff4f5e55fe7aa51af13 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Mon, 1 Sep 2025 16:27:15 -0700 Subject: [PATCH 10/17] Cleanup --- Sources/Packages/Localizable.xcstrings | 2 +- .../Secretive/Views/IntegrationsView.swift | 239 ++++++++++-------- 2 files changed, 135 insertions(+), 106 deletions(-) diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index ed18ceb..ecc590c 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -3049,7 +3049,7 @@ "Getting Started" : { }, - "If you don't known what shell you use and haven't changed it, you're probably using `zsh`." : { + "If you don't known what shell you use and haven't changed it, you're probably using `%@`." : { }, "If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client." : { diff --git a/Sources/Secretive/Views/IntegrationsView.swift b/Sources/Secretive/Views/IntegrationsView.swift index 7cef900..1d4cef6 100644 --- a/Sources/Secretive/Views/IntegrationsView.swift +++ b/Sources/Secretive/Views/IntegrationsView.swift @@ -5,13 +5,12 @@ struct IntegrationsView: View { @Environment(\.dismiss) private var dismiss @State private var selectedInstruction: ConfigurationFileInstructions? - - private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String + private let instructions = Instructions() var body: some View { NavigationSplitView { List(selection: $selectedInstruction) { - ForEach(instructions) { group in + ForEach(instructions.instructions) { group in Section(group.name) { ForEach(group.instructions) { instruction in Text(instruction.tool) @@ -22,104 +21,10 @@ struct IntegrationsView: View { } } } detail: { - if let selectedInstruction { - switch selectedInstruction.id { - case .gettingStarted: - Form { - Section("Configuring Tools for Secretive") { - Text("Most tools will try and look for SSH keys on disk in `~/.ssh`. To use Secretive, we need to configure those tools to talk to Secretive instead.") - } - Section { - NavigationLink(value: ssh) { - Text("If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client.") - } - NavigationLink(value: zsh) { - VStack(alignment: .leading) { - Text("If you're trying to configure anything your command line runs to use Secretive, configure your shell.") - Text("If you don't known what shell you use and haven't changed it, you're probably using `zsh`.") - .font(.caption2) - .foregroundStyle(.secondary) - } - } - NavigationLink(value: git) { - Text("If you're trying to sign your git commits, set up Git Signing.") - } - } header: { - Text("What Should I Configure?") - } - footer: { - Text("You can configure more than one tool, they generally won't interfere with each other.") - } - } - .formStyle(.grouped) - case .tool: - Form { - ForEach(selectedInstruction.steps) { stepGroup in - Section { - ConfigurationItemView(title: "Configuration File", value: stepGroup.path, action: .revealInFinder(stepGroup.path)) - ForEach(stepGroup.steps, id: \.self) { step in - ConfigurationItemView(title: "Add This:", action: .copy(step)) { - HStack { - Text(step) - .padding(8) - .font(.system(.subheadline, design: .monospaced)) - Spacer() - } - .frame(maxWidth: .infinity) - .background { - RoundedRectangle(cornerRadius: 6) - .fill(.black.opacity(0.05)) - .stroke(.separator, lineWidth: 1) - } - } - } - } footer: { - if let note = stepGroup.note { - Text(note) - .font(.caption) - } - } - } - if let url = selectedInstruction.website { - Section { - Link(destination: url) { - VStack(alignment: .leading, spacing: 5) { - Text("View Documentation on Web") - .font(.headline) - Text(url.absoluteString) - .font(.caption2) - } - } - } - } - } - .formStyle(.grouped) - case .otherShell: - Form { - Section { - Link("View on GitHub", destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!) - } header: { - Text("There's a community-maintained list of shell instructions on GitHub. If the shell you're looking for isn't supported, create an issue and the community may be able to help.") - .font(.body) - } - } - .formStyle(.grouped) - - case .otherApp: - Form { - Section { - Link("View on GitHub", destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!) - } header: { - Text("There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help.") - .font(.body) - } - } - .formStyle(.grouped) - } - } + IntegrationsDetailView(selectedInstruction: $selectedInstruction) } .onAppear { - selectedInstruction = instructions.first?.instructions.first + selectedInstruction = instructions.gettingStarted } .toolbar { ToolbarItem(placement: .primaryAction) { @@ -133,9 +38,133 @@ struct IntegrationsView: View { } -extension IntegrationsView { +struct IntegrationsDetailView: View { - fileprivate var ssh: ConfigurationFileInstructions { + @Binding private var selectedInstruction: ConfigurationFileInstructions? + private let instructions = Instructions() + + init(selectedInstruction: Binding) { + _selectedInstruction = selectedInstruction + } + + var body: some View { + if let selectedInstruction { + switch selectedInstruction.id { + case .gettingStarted: + Form { + Section("Configuring Tools for Secretive") { + Text("Most tools will try and look for SSH keys on disk in `~/.ssh`. To use Secretive, we need to configure those tools to talk to Secretive instead.") + } + Section { + Group { + Text("If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client.") + .onTapGesture { + self.selectedInstruction = instructions.ssh + } + VStack(alignment: .leading, spacing: 5) { + Text("If you're trying to configure anything your command line runs to use Secretive, configure your shell.") + Text("If you don't known what shell you use and haven't changed it, you're probably using `\(instructions.defaultShell.tool)`.") + .font(.caption2) + } + .onTapGesture { + self.selectedInstruction = instructions.defaultShell + } + Text("If you're trying to sign your git commits, set up Git Signing.") + .onTapGesture { + self.selectedInstruction = instructions.git + } + } + .foregroundStyle(.link) + + } header: { + Text("What Should I Configure?") + } + footer: { + Text("You can configure more than one tool, they generally won't interfere with each other.") + } + } + .formStyle(.grouped) + case .tool: + Form { + ForEach(selectedInstruction.steps) { stepGroup in + Section { + ConfigurationItemView(title: "Configuration File", value: stepGroup.path, action: .revealInFinder(stepGroup.path)) + ForEach(stepGroup.steps, id: \.self) { step in + ConfigurationItemView(title: "Add This:", action: .copy(step)) { + HStack { + Text(step) + .padding(8) + .font(.system(.subheadline, design: .monospaced)) + Spacer() + } + .frame(maxWidth: .infinity) + .background { + RoundedRectangle(cornerRadius: 6) + .fill(.black.opacity(0.05)) + .stroke(.separator, lineWidth: 1) + } + } + } + } footer: { + if let note = stepGroup.note { + Text(note) + .font(.caption) + } + } + } + if let url = selectedInstruction.website { + Section { + Link(destination: url) { + VStack(alignment: .leading, spacing: 5) { + Text("View Documentation on Web") + .font(.headline) + Text(url.absoluteString) + .font(.caption2) + } + } + } + } + } + .formStyle(.grouped) + case .otherShell: + Form { + Section { + Link("View on GitHub", destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!) + } header: { + Text("There's a community-maintained list of shell instructions on GitHub. If the shell you're looking for isn't supported, create an issue and the community may be able to help.") + .font(.body) + } + } + .formStyle(.grouped) + + case .otherApp: + Form { + Section { + Link("View on GitHub", destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!) + } header: { + Text("There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help.") + .font(.body) + } + } + .formStyle(.grouped) + } + } + + } +} + +private struct Instructions { + + private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String + + + var defaultShell: ConfigurationFileInstructions { + zsh + } + + var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions("Getting Started", id: .gettingStarted) + + var ssh: ConfigurationFileInstructions { ConfigurationFileInstructions( tool: "SSH", configPath: "~/.ssh/config", @@ -145,7 +174,7 @@ extension IntegrationsView { ) } - fileprivate var git: ConfigurationFileInstructions { + var git: ConfigurationFileInstructions { ConfigurationFileInstructions( tool: "Git Signing", steps: [ @@ -176,7 +205,7 @@ extension IntegrationsView { ) } - fileprivate var zsh: ConfigurationFileInstructions { + var zsh: ConfigurationFileInstructions { ConfigurationFileInstructions( tool: "zsh", configPath: "~/.zshrc", @@ -184,10 +213,10 @@ extension IntegrationsView { ) } - fileprivate var instructions: [ConfigurationGroup] { + var instructions: [ConfigurationGroup] { [ ConfigurationGroup(name:"Integrations", instructions: [ - ConfigurationFileInstructions("Getting Started", id: .gettingStarted), + gettingStarted ]), ConfigurationGroup( name: "System", From f3ce6b9d0fd05c4b27c83d47a9343989edf3a72d Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Mon, 1 Sep 2025 17:43:33 -0700 Subject: [PATCH 11/17] WIP --- Sources/Packages/Localizable.xcstrings | 9 ++ Sources/Secretive/App.swift | 6 +- .../Secretive/Views/ActionButtonStyle.swift | 29 +++++- Sources/Secretive/Views/AgentStatusView.swift | 2 +- Sources/Secretive/Views/ContentView.swift | 2 +- .../Secretive/Views/CreateSecretView.swift | 2 +- Sources/Secretive/Views/EditSecretView.swift | 2 +- .../Secretive/Views/IntegrationsView.swift | 42 +++++++-- Sources/Secretive/Views/SetupView.swift | 94 ++++++++++++------- 9 files changed, 138 insertions(+), 50 deletions(-) diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index ecc590c..9a78244 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -1186,6 +1186,9 @@ }, "Configure" : { + }, + "Configure Integrations" : { + }, "Configuring Tools for Secretive" : { @@ -3974,6 +3977,9 @@ } } } + }, + "Setup" : { + }, "setup_agent_activity_monitor_description" : { "extractionState" : "manual", @@ -5287,6 +5293,9 @@ }, "System" : { + }, + "Tell the tools you use how to talk to Secretive." : { + }, "There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help." : { diff --git a/Sources/Secretive/App.swift b/Sources/Secretive/App.swift index bbd25c4..7e6c83d 100644 --- a/Sources/Secretive/App.swift +++ b/Sources/Secretive/App.swift @@ -61,7 +61,6 @@ struct Secretive: App { } .sheet(isPresented: $showingIntegrations) { IntegrationsView() - .frame(minHeight: 400) } } .commands { @@ -81,6 +80,11 @@ struct Secretive: App { NSWorkspace.shared.open(Constants.helpURL) } } + CommandGroup(after: .help) { + Button("Setup") { + showingSetup = true + } + } SidebarCommands() } } diff --git a/Sources/Secretive/Views/ActionButtonStyle.swift b/Sources/Secretive/Views/ActionButtonStyle.swift index 1f6afdc..74284a7 100644 --- a/Sources/Secretive/Views/ActionButtonStyle.swift +++ b/Sources/Secretive/Views/ActionButtonStyle.swift @@ -3,10 +3,11 @@ import SwiftUI struct PrimaryButtonModifier: ViewModifier { @Environment(\.colorScheme) var colorScheme + @Environment(\.isEnabled) var isEnabled func body(content: Content) -> some View { // Tinted glass prominent is really hard to read on 26.0. - if #available(macOS 26.0, *), colorScheme == .dark { + if #available(macOS 26.0, *), colorScheme == .dark, isEnabled { content.buttonStyle(.glassProminent) } else { content.buttonStyle(.borderedProminent) @@ -17,13 +18,13 @@ struct PrimaryButtonModifier: ViewModifier { extension View { - func primary() -> some View { + func primaryButton() -> some View { modifier(PrimaryButtonModifier()) } } -struct NormalButtonModifier: ViewModifier { +struct MenuButtonModifier: ViewModifier { func body(content: Content) -> some View { if #available(macOS 26.0, *) { @@ -39,7 +40,27 @@ struct NormalButtonModifier: ViewModifier { extension View { - func normal() -> some View { + func menuButton() -> some View { + modifier(MenuButtonModifier()) + } + +} + +struct NormalButtonModifier: ViewModifier { + + func body(content: Content) -> some View { + if #available(macOS 26.0, *) { + content.buttonStyle(.glass) + } else { + content.buttonStyle(.bordered) + } + } + +} + +extension View { + + func normalButton() -> some View { modifier(NormalButtonModifier()) } diff --git a/Sources/Secretive/Views/AgentStatusView.swift b/Sources/Secretive/Views/AgentStatusView.swift index 6af8df5..931f449 100644 --- a/Sources/Secretive/Views/AgentStatusView.swift +++ b/Sources/Secretive/Views/AgentStatusView.swift @@ -127,7 +127,7 @@ struct AgentNotRunningView: View { } } } - .primary() + .primaryButton() } 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() diff --git a/Sources/Secretive/Views/ContentView.swift b/Sources/Secretive/Views/ContentView.swift index 8ef73a7..933013d 100644 --- a/Sources/Secretive/Views/ContentView.swift +++ b/Sources/Secretive/Views/ContentView.swift @@ -106,7 +106,7 @@ extension ContentView { Button(.appMenuNewSecretButton, systemImage: "plus") { showingCreation = true } - .normal() + .menuButton() .sheet(isPresented: $showingCreation) { if let modifiable = storeList.modifiableStore { CreateSecretView(store: modifiable) { created in diff --git a/Sources/Secretive/Views/CreateSecretView.swift b/Sources/Secretive/Views/CreateSecretView.swift index f3e8b03..bd317ae 100644 --- a/Sources/Secretive/Views/CreateSecretView.swift +++ b/Sources/Secretive/Views/CreateSecretView.swift @@ -113,7 +113,7 @@ struct CreateSecretView: View { } Button(.createSecretCreateButton, action: save) .keyboardShortcut(.return) - .primary() + .primaryButton() .disabled(name.isEmpty) } .padding() diff --git a/Sources/Secretive/Views/EditSecretView.swift b/Sources/Secretive/Views/EditSecretView.swift index 06dde39..0b2a6fc 100644 --- a/Sources/Secretive/Views/EditSecretView.swift +++ b/Sources/Secretive/Views/EditSecretView.swift @@ -45,7 +45,7 @@ struct EditSecretView: View { Button(.editSaveButton, action: rename) .disabled(name.isEmpty) .keyboardShortcut(.return) - .primary() + .primaryButton() } .padding() } diff --git a/Sources/Secretive/Views/IntegrationsView.swift b/Sources/Secretive/Views/IntegrationsView.swift index 1d4cef6..3c3439c 100644 --- a/Sources/Secretive/Views/IntegrationsView.swift +++ b/Sources/Secretive/Views/IntegrationsView.swift @@ -21,19 +21,47 @@ struct IntegrationsView: View { } } } detail: { - IntegrationsDetailView(selectedInstruction: $selectedInstruction) + IntegrationsDetailView(selectedInstruction: $selectedInstruction) + .fauxToolbar { + Button("Done") { + dismiss() + } + .normalButton() + } } .onAppear { selectedInstruction = instructions.gettingStarted } - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button("Done") { - dismiss() - } - .styled + .frame(minHeight: 500) + } + +} + +extension View { + + func fauxToolbar(content: () -> Content) -> some View { + modifier(FauxToolbarModifier(toolbarContent: content())) + } + +} + +struct FauxToolbarModifier: ViewModifier { + + var toolbarContent: ToolbarContent + + func body(content: Content) -> some View { + VStack(alignment: .leading) { + content + Divider() + HStack { + Spacer() + toolbarContent + .padding(.top, 8) + .padding(.trailing, 16) + .padding(.bottom, 16) } } + } } diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift index eaa1652..0adc864 100644 --- a/Sources/Secretive/Views/SetupView.swift +++ b/Sources/Secretive/Views/SetupView.swift @@ -4,20 +4,26 @@ struct SetupView: View { @Environment(\.dismiss) private var dismiss @Binding var setupComplete: Bool - + + @State var showingIntegrations = false + @State var buttonWidth: CGFloat? + @State var installed = false @State var updates = false - @State var sshConfig = false + @State var integrations = false + var allDone: Bool { + installed && updates && integrations + } var body: some View { VStack { - VStack(spacing: 0) { + VStack(alignment: .leading, spacing: 0) { StepView( title: "setup_agent_title", description: "setup_agent_description", systemImage: "lock.laptopcomputer", ) { - OnboardingButton("setup_agent_install_button", installed) { + OnboardingButton("setup_agent_install_button", installed, width: buttonWidth) { Task { installed = await LaunchAgentController().install() } @@ -29,78 +35,97 @@ struct SetupView: View { description: "setup_updates_description", systemImage: "network.badge.shield.half.filled", ) { - OnboardingButton("setup_updates_ok", updates) { - Task { - updates = true - } + OnboardingButton("setup_updates_ok", updates, width: buttonWidth) { + updates = true } } Divider() StepView( - title: "setup_ssh_title", - description: "setup_ssh_description", - systemImage: "network.badge.shield.half.filled", + title: "Configure Integrations", + description: "Tell the tools you use how to talk to Secretive.", + systemImage: "firewall", ) { - HStack { - OnboardingButton("Configure", false) { -// sshConfig = true - } + OnboardingButton("Configure", integrations, width: buttonWidth) { + showingIntegrations = true } } } + .onPreferenceChange(OnboardingButton.WidthKey.self) { width in + buttonWidth = width + } .background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10)) .frame(minWidth: 700, maxWidth: .infinity) HStack { Spacer() - Button("Done") {} - .styled + Button("Done") { + setupComplete = true + dismiss() + } + .disabled(!allDone) + .primaryButton() } } + .interactiveDismissDisabled() .padding() + .sheet(isPresented: $showingIntegrations, onDismiss: { + integrations = true + }, content: { + IntegrationsView() + }) } } struct OnboardingButton: View { + struct WidthKey: @MainActor PreferenceKey { + @MainActor static var defaultValue: CGFloat? = nil + static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) { + if let next = nextValue(), next > (value ?? -1) { + value = next + } + } + + } + let label: LocalizedStringResource let complete: Bool let action: () -> Void - - init(_ label: LocalizedStringResource, _ complete: Bool, action: @escaping () -> Void) { + let width: CGFloat? + @State var currentWidth: CGFloat? + + init(_ label: LocalizedStringResource, _ complete: Bool, width: CGFloat? = nil, action: @escaping () -> Void) { self.label = label self.complete = complete self.action = action + self.width = width } var body: some View { Button(action: action) { HStack(spacing: 6) { - Text(label) if complete { + Text("Done") Image(systemName: "checkmark.circle.fill") + } else { + Text(label) } } + .frame(width: width) .padding(.vertical, 2) + .onGeometryChange(for: CGFloat.self) { proxy in + proxy.size.width + } action: { newValue in + currentWidth = newValue + } } + .preference(key: WidthKey.self, value: currentWidth) + .primaryButton() .disabled(complete) - .styled + .tint(complete ? .green : nil) } } -extension View { - - @ViewBuilder - var styled: some View { - if #available(macOS 26.0, *) { - buttonStyle(.glassProminent) - } else { - buttonStyle(.borderedProminent) - } - } - -} - struct StepView: View { let title: LocalizedStringResource @@ -126,6 +151,7 @@ struct StepView: View { .bold() Text(description) } + Spacer() actions } .padding(20) From a640d11b00bfeb92e709f9cd31487dd4c7c08ab2 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Mon, 1 Sep 2025 17:50:40 -0700 Subject: [PATCH 12/17] WIP --- Sources/Secretive/Views/SetupView.swift | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift index 0adc864..0eed09f 100644 --- a/Sources/Secretive/Views/SetupView.swift +++ b/Sources/Secretive/Views/SetupView.swift @@ -23,9 +23,14 @@ struct SetupView: View { description: "setup_agent_description", systemImage: "lock.laptopcomputer", ) { - OnboardingButton("setup_agent_install_button", installed, width: buttonWidth) { + OnboardingButton( + "setup_agent_install_button", + complete: installed, + width: buttonWidth + ) { + installed = true Task { - installed = await LaunchAgentController().install() + await LaunchAgentController().install() } } } @@ -35,7 +40,11 @@ struct SetupView: View { description: "setup_updates_description", systemImage: "network.badge.shield.half.filled", ) { - OnboardingButton("setup_updates_ok", updates, width: buttonWidth) { + OnboardingButton( + "setup_updates_ok", + complete: updates, + width: buttonWidth + ) { updates = true } } @@ -45,7 +54,11 @@ struct SetupView: View { description: "Tell the tools you use how to talk to Secretive.", systemImage: "firewall", ) { - OnboardingButton("Configure", integrations, width: buttonWidth) { + OnboardingButton( + "Configure", + complete: integrations, + width: buttonWidth + ) { showingIntegrations = true } } @@ -93,7 +106,7 @@ struct OnboardingButton: View { let width: CGFloat? @State var currentWidth: CGFloat? - init(_ label: LocalizedStringResource, _ complete: Bool, width: CGFloat? = nil, action: @escaping () -> Void) { + init(_ label: LocalizedStringResource, complete: Bool, width: CGFloat? = nil, action: @escaping () -> Void) { self.label = label self.complete = complete self.action = action From 90d55726bb779b3535584160f43e6af2bab2f0a5 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Mon, 1 Sep 2025 18:00:58 -0700 Subject: [PATCH 13/17] WIP --- Sources/Packages/Localizable.xcstrings | 328 ++++++++++++++++-- .../Secretive/Views/IntegrationsView.swift | 2 +- 2 files changed, 291 insertions(+), 39 deletions(-) diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index 9a78244..503e754 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -3,9 +3,6 @@ "strings" : { "" : { - }, - "Add This:" : { - }, "agent_not_running_notice_detail_description" : { "extractionState" : "manual", @@ -789,7 +786,14 @@ } }, "Apps" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apps" + } + } + } }, "auth_context_persist_for_duration" : { "comment" : "When the user clicks the notification to leave a secret unlocked, they are shown a prompt to approve the action. This is the description, showing which secret will used. The first placeholder is the name of the secret. The second placeholder is a localized description of the time period it will remain unlocked for (eg: \"five minutes\")", @@ -1182,19 +1186,54 @@ } }, "Configuration File" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuration File" + } + } + } }, "Configure" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configure" + } + } + } }, "Configure Integrations" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configure Integrations" + } + } + } }, "Configuring Tools for Secretive" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuring Tools for Secretive" + } + } + } }, "Copy" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy" + } + } + } }, "copyable_click_to_copy_button" : { "extractionState" : "manual", @@ -2517,10 +2556,24 @@ } }, "Disable Agent" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disable Agent" + } + } + } }, "Done" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Done" + } + } + } }, "edit_cancel_button" : { "extractionState" : "manual", @@ -3050,28 +3103,94 @@ } }, "Getting Started" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Getting Started" + } + } + } }, "If you don't known what shell you use and haven't changed it, you're probably using `%@`." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you don't known what shell you use and haven't changed it, you're probably using `%@`." + } + } + } }, "If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client." + } + } + } }, "If you're trying to configure anything your command line runs to use Secretive, configure your shell." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you're trying to configure anything your command line runs to use Secretive, configure your shell." + } + } + } }, "If you're trying to sign your git commits, set up Git Signing." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you're trying to sign your git commits, set up Git Signing." + } + } + } }, "Integrations" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Integrations" + } + } + } + }, + "integrations_add_this_title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add This:" + } + } + } }, "Integrations..." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add This:" + } + } + } }, "Most tools will try and look for SSH keys on disk in `~/.ssh`. To use Secretive, we need to configure those tools to talk to Secretive instead." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Most tools will try and look for SSH keys on disk in `~/.ssh`. To use Secretive, we need to configure those tools to talk to Secretive instead." + } + } + } }, "no_secure_storage_description" : { "extractionState" : "manual", @@ -3467,16 +3586,44 @@ } }, "Restart Agent" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restart Agent" + } + } + } }, "Reveal in Finder" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reveal in Finder" + } + } + } }, "Running Since" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Running Since" + } + } + } }, "Secret Agent Location" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secret Agent Location" + } + } + } }, "secret_detail_md5_fingerprint_label" : { "extractionState" : "manual", @@ -3905,7 +4052,14 @@ } }, "Secretive was unable to get SecretAgent to launch. Please try restarting your Mac, and if that doesn't work, file an issue on GitHub." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secretive was unable to get SecretAgent to launch. Please try restarting your Mac, and if that doesn't work, file an issue on GitHub." + } + } + } }, "secure_enclave" : { "extractionState" : "manual", @@ -3979,7 +4133,14 @@ } }, "Setup" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Setup" + } + } + } }, "setup_agent_activity_monitor_description" : { "extractionState" : "manual", @@ -5053,7 +5214,14 @@ } }, "Shell" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shell" + } + } + } }, "signed_notification_description" : { "comment" : "When the user performs an action using a secret, they're shown a notification describing what happened. This is the description, showing which secret was used. The placeholder is the name of the secret.", @@ -5283,25 +5451,74 @@ } }, "Socket Path" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Socket Path" + } + } + } }, "Start Agent" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start Agent" + } + } + } }, "Starting Agent" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Starting Agent" + } + } + } }, "System" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "System" + } + } + } }, "Tell the tools you use how to talk to Secretive." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tell the tools you use how to talk to Secretive." + } + } + } }, "There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help." + } + } + } }, "There's a community-maintained list of shell instructions on GitHub. If the shell you're looking for isn't supported, create an issue and the community may be able to help." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "There's a community-maintained list of shell instructions on GitHub. If the shell you're looking for isn't supported, create an issue and the community may be able to help." + } + } + } }, "unnamed_secret" : { "extractionState" : "manual", @@ -6250,19 +6467,54 @@ } }, "Version" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Version" + } + } + } }, "View Documentation on Web" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "View Documentation on Web" + } + } + } }, "View on GitHub" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "View on GitHub" + } + } + } }, "What Should I Configure?" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "What Should I Configure?" + } + } + } }, "You can configure more than one tool, they generally won't interfere with each other." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You can configure more than one tool, they generally won't interfere with each other." + } + } + } } }, "version" : "1.0" diff --git a/Sources/Secretive/Views/IntegrationsView.swift b/Sources/Secretive/Views/IntegrationsView.swift index 3c3439c..fa53196 100644 --- a/Sources/Secretive/Views/IntegrationsView.swift +++ b/Sources/Secretive/Views/IntegrationsView.swift @@ -118,7 +118,7 @@ struct IntegrationsDetailView: View { Section { ConfigurationItemView(title: "Configuration File", value: stepGroup.path, action: .revealInFinder(stepGroup.path)) ForEach(stepGroup.steps, id: \.self) { step in - ConfigurationItemView(title: "Add This:", action: .copy(step)) { + ConfigurationItemView(title: "integrations_add_this_title", action: .copy(step)) { HStack { Text(step) .padding(8) From 0980cdffcd585348fea3a91c115430e90c0b8ee0 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Mon, 1 Sep 2025 19:25:14 -0700 Subject: [PATCH 14/17] WIP --- Sources/Packages/Localizable.xcstrings | 549 +++++++++--------- Sources/Secretive/Views/AgentStatusView.swift | 18 +- .../Secretive/Views/IntegrationsView.swift | 34 +- Sources/Secretive/Views/SetupView.swift | 6 +- 4 files changed, 317 insertions(+), 290 deletions(-) diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index 503e754..7022a66 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -3,6 +3,94 @@ "strings" : { "" : { + }, + "agent_details_could_not_start_error" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secretive was unable to get SecretAgent to launch. Please try restarting your Mac, and if that doesn't work, file an issue on GitHub." + } + } + } + }, + "agent_details_disable_agent_button" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disable Agent" + } + } + } + }, + "agent_details_restart_agent_button" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restart Agent" + } + } + } + }, + "agent_details_running_since_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Running Since" + } + } + } + }, + "agent_details_socket_path_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Socket Path" + } + } + } + }, + "agent_details_start_agent_button" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start Agent" + } + } + } + }, + "agent_details_start_agent_button_starting" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Starting Agent" + } + } + } + }, + "agent_details_version_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Version" + } + } + } }, "agent_not_running_notice_detail_description" : { "extractionState" : "manual", @@ -400,6 +488,17 @@ } } }, + "agentDetailsLocationTitle" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secret Agent Location" + } + } + } + }, "app_menu_help_button" : { "extractionState" : "manual", "localizations" : { @@ -785,16 +884,6 @@ } } }, - "Apps" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apps" - } - } - } - }, "auth_context_persist_for_duration" : { "comment" : "When the user clicks the notification to leave a secret unlocked, they are shown a prompt to approve the action. This is the description, showing which secret will used. The first placeholder is the name of the secret. The second placeholder is a localized description of the time period it will remain unlocked for (eg: \"five minutes\")", "extractionState" : "manual", @@ -1185,46 +1274,6 @@ } } }, - "Configuration File" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuration File" - } - } - } - }, - "Configure" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configure" - } - } - } - }, - "Configure Integrations" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configure Integrations" - } - } - } - }, - "Configuring Tools for Secretive" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuring Tools for Secretive" - } - } - } - }, "Copy" : { "localizations" : { "en" : { @@ -2555,16 +2604,6 @@ } } }, - "Disable Agent" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Disable Agent" - } - } - } - }, "Done" : { "localizations" : { "en" : { @@ -3112,46 +3151,6 @@ } } }, - "If you don't known what shell you use and haven't changed it, you're probably using `%@`." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "If you don't known what shell you use and haven't changed it, you're probably using `%@`." - } - } - } - }, - "If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client." - } - } - } - }, - "If you're trying to configure anything your command line runs to use Secretive, configure your shell." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "If you're trying to configure anything your command line runs to use Secretive, configure your shell." - } - } - } - }, - "If you're trying to sign your git commits, set up Git Signing." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "If you're trying to sign your git commits, set up Git Signing." - } - } - } - }, "Integrations" : { "localizations" : { "en" : { @@ -3163,6 +3162,7 @@ } }, "integrations_add_this_title" : { + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -3172,22 +3172,78 @@ } } }, + "integrations_apps_row_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apps" + } + } + } + }, + "integrations_community_apps_list_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help." + } + } + } + }, + "integrations_community_shell_list_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "There's a community-maintained list of shell instructions on GitHub. If the shell you're looking for isn't supported, create an issue and the community may be able to help." + } + } + } + }, + "integrations_path_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuration File" + } + } + } + }, + "integrations_view_other_github_link" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "View on GitHub" + } + } + } + }, + "integrations_web_link" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "View Documentation on Web" + } + } + } + }, "Integrations..." : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Add This:" - } - } - } - }, - "Most tools will try and look for SSH keys on disk in `~/.ssh`. To use Secretive, we need to configure those tools to talk to Secretive instead." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Most tools will try and look for SSH keys on disk in `~/.ssh`. To use Secretive, we need to configure those tools to talk to Secretive instead." + "value" : "Integrations…" } } } @@ -3423,6 +3479,127 @@ } } }, + "onboarding_getting_started_multiple_config" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You can configure more than one tool, they generally won't interfere with each other." + } + } + } + }, + "onboarding_getting_started_suggestion_git" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you're trying to sign your git commits, set up Git Signing." + } + } + } + }, + "onboarding_getting_started_suggestion_shell" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you're trying to configure anything your command line runs to use Secretive, configure your shell." + } + } + } + }, + "onboarding_getting_started_suggestion_shell_default" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you don't known what shell you use and haven't changed it, you're probably using `%(shellName)@`." + } + } + } + }, + "onboarding_getting_started_suggestion_ssh" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client." + } + } + } + }, + "onboarding_getting_started_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuring Tools for Secretive" + } + } + } + }, + "onboarding_getting_started_title_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Most tools will try and look for SSH keys on disk in `~/.ssh`. To use Secretive, we need to configure those tools to talk to Secretive instead." + } + } + } + }, + "onboarding_getting_started_what_should_i_configure_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "What Should I Configure?" + } + } + } + }, + "onboarding_integrations_button_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configure" + } + } + } + }, + "onboarding_integrations_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tell the tools you use how to talk to Secretive." + } + } + } + }, + "onboarding_integrations_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configure Integrations" + } + } + } + }, "other" : { }, @@ -3585,16 +3762,6 @@ } } }, - "Restart Agent" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Restart Agent" - } - } - } - }, "Reveal in Finder" : { "localizations" : { "en" : { @@ -3605,26 +3772,6 @@ } } }, - "Running Since" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Running Since" - } - } - } - }, - "Secret Agent Location" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Secret Agent Location" - } - } - } - }, "secret_detail_md5_fingerprint_label" : { "extractionState" : "manual", "localizations" : { @@ -4051,16 +4198,6 @@ } } }, - "Secretive was unable to get SecretAgent to launch. Please try restarting your Mac, and if that doesn't work, file an issue on GitHub." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Secretive was unable to get SecretAgent to launch. Please try restarting your Mac, and if that doesn't work, file an issue on GitHub." - } - } - } - }, "secure_enclave" : { "extractionState" : "manual", "localizations" : { @@ -5450,36 +5587,6 @@ } } }, - "Socket Path" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Socket Path" - } - } - } - }, - "Start Agent" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Start Agent" - } - } - } - }, - "Starting Agent" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Starting Agent" - } - } - } - }, "System" : { "localizations" : { "en" : { @@ -5490,36 +5597,6 @@ } } }, - "Tell the tools you use how to talk to Secretive." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tell the tools you use how to talk to Secretive." - } - } - } - }, - "There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help." - } - } - } - }, - "There's a community-maintained list of shell instructions on GitHub. If the shell you're looking for isn't supported, create an issue and the community may be able to help." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "There's a community-maintained list of shell instructions on GitHub. If the shell you're looking for isn't supported, create an issue and the community may be able to help." - } - } - } - }, "unnamed_secret" : { "extractionState" : "manual", "localizations" : { @@ -6465,56 +6542,6 @@ } } } - }, - "Version" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Version" - } - } - } - }, - "View Documentation on Web" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "View Documentation on Web" - } - } - } - }, - "View on GitHub" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "View on GitHub" - } - } - } - }, - "What Should I Configure?" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "What Should I Configure?" - } - } - } - }, - "You can configure more than one tool, they generally won't interfere with each other." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "You can configure more than one tool, they generally won't interfere with each other." - } - } - } } }, "version" : "1.0" diff --git a/Sources/Secretive/Views/AgentStatusView.swift b/Sources/Secretive/Views/AgentStatusView.swift index 931f449..082eccf 100644 --- a/Sources/Secretive/Views/AgentStatusView.swift +++ b/Sources/Secretive/Views/AgentStatusView.swift @@ -22,22 +22,22 @@ struct AgentRunningView: View { Section { if let process = agentStatusChecker.process { ConfigurationItemView( - title: "Secret Agent Location", + title: LocalizedStringResource.agentDetailsLocationTitle, value: process.bundleURL!.path(), action: .revealInFinder(process.bundleURL!.path()), ) ConfigurationItemView( - title: "Socket Path", + title: LocalizedStringResource.agentDetailsSocketPathTitle, value: socketPath, action: .copy(socketPath), ) ConfigurationItemView( - title: "Version", + title: LocalizedStringResource.agentDetailsVersionTitle, value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String ) if let launchDate = process.launchDate { ConfigurationItemView( - title: "Running Since", + title: LocalizedStringResource.agentDetailsRunningSinceTitle, value: launchDate.formatted() ) } @@ -51,8 +51,8 @@ struct AgentRunningView: View { Text(.agentRunningNoticeDetailDescription) HStack { Spacer() - Menu("Restart Agent") { - Button("Disable Agent") { + Menu(.agentDetailsRestartAgentButton) { + Button(.agentDetailsDisableAgentButton) { Task { _ = await LaunchAgentController() .uninstall() @@ -118,10 +118,10 @@ struct AgentNotRunningView: View { } } label: { if !loading { - Text("Start Agent") + Text(.agentDetailsStartAgentButton) } else { HStack { - Text("Starting Agent") + Text(.agentDetailsStartAgentButtonStarting) ProgressView() .controlSize(.mini) } @@ -129,7 +129,7 @@ struct AgentNotRunningView: View { } .primaryButton() } 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.") + Text(.agentDetailsCouldNotStartError) .bold() .foregroundStyle(.red) } diff --git a/Sources/Secretive/Views/IntegrationsView.swift b/Sources/Secretive/Views/IntegrationsView.swift index fa53196..01be401 100644 --- a/Sources/Secretive/Views/IntegrationsView.swift +++ b/Sources/Secretive/Views/IntegrationsView.swift @@ -80,24 +80,24 @@ struct IntegrationsDetailView: View { switch selectedInstruction.id { case .gettingStarted: Form { - Section("Configuring Tools for Secretive") { - Text("Most tools will try and look for SSH keys on disk in `~/.ssh`. To use Secretive, we need to configure those tools to talk to Secretive instead.") + Section(.onboardingGettingStartedTitle) { + Text(.onboardingGettingStartedTitleDescription) } Section { Group { - Text("If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client.") + Text(.onboardingGettingStartedSuggestionSsh) .onTapGesture { self.selectedInstruction = instructions.ssh } VStack(alignment: .leading, spacing: 5) { - Text("If you're trying to configure anything your command line runs to use Secretive, configure your shell.") - Text("If you don't known what shell you use and haven't changed it, you're probably using `\(instructions.defaultShell.tool)`.") + Text(.onboardingGettingStartedSuggestionShell) + Text(.onboardingGettingStartedSuggestionShellDefault(shellName: instructions.defaultShell.tool)) .font(.caption2) } .onTapGesture { self.selectedInstruction = instructions.defaultShell } - Text("If you're trying to sign your git commits, set up Git Signing.") + Text(.onboardingGettingStartedSuggestionGit) .onTapGesture { self.selectedInstruction = instructions.git } @@ -105,10 +105,10 @@ struct IntegrationsDetailView: View { .foregroundStyle(.link) } header: { - Text("What Should I Configure?") + Text(.onboardingGettingStartedWhatShouldIConfigureTitle) } footer: { - Text("You can configure more than one tool, they generally won't interfere with each other.") + Text(.onboardingGettingStartedMultipleConfig) } } .formStyle(.grouped) @@ -116,9 +116,9 @@ struct IntegrationsDetailView: View { Form { ForEach(selectedInstruction.steps) { stepGroup in Section { - ConfigurationItemView(title: "Configuration File", value: stepGroup.path, action: .revealInFinder(stepGroup.path)) + ConfigurationItemView(title: LocalizedStringResource.integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path)) ForEach(stepGroup.steps, id: \.self) { step in - ConfigurationItemView(title: "integrations_add_this_title", action: .copy(step)) { + ConfigurationItemView(title: LocalizedStringResource.integrationsAddThisTitle, action: .copy(step)) { HStack { Text(step) .padding(8) @@ -144,7 +144,7 @@ struct IntegrationsDetailView: View { Section { Link(destination: url) { VStack(alignment: .leading, spacing: 5) { - Text("View Documentation on Web") + Text(.integrationsWebLink) .font(.headline) Text(url.absoluteString) .font(.caption2) @@ -157,9 +157,9 @@ struct IntegrationsDetailView: View { case .otherShell: Form { Section { - Link("View on GitHub", destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!) + Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!) } header: { - Text("There's a community-maintained list of shell instructions on GitHub. If the shell you're looking for isn't supported, create an issue and the community may be able to help.") + Text(.integrationsCommunityShellListDescription) .font(.body) } } @@ -168,9 +168,9 @@ struct IntegrationsDetailView: View { case .otherApp: Form { Section { - Link("View on GitHub", destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!) + Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!) } header: { - Text("There's a community-maintained list of instructions for apps on GitHub. If the app you're looking for isn't supported, create an issue and the community may be able to help.") + Text(.integrationsCommunityAppsListDescription) .font(.body) } } @@ -243,7 +243,7 @@ private struct Instructions { var instructions: [ConfigurationGroup] { [ - ConfigurationGroup(name:"Integrations", instructions: [ + ConfigurationGroup(name: "Integrations", instructions: [ gettingStarted ]), ConfigurationGroup( @@ -268,7 +268,7 @@ private struct Instructions { ConfigurationFileInstructions("other", id: .otherShell), ]), ConfigurationGroup(name: "Other", instructions: [ - ConfigurationFileInstructions("Apps", id: .otherApp), + ConfigurationFileInstructions(LocalizedStringResource.integrationsAppsRowTitle, id: .otherApp), ]), ] } diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift index 0eed09f..ad26638 100644 --- a/Sources/Secretive/Views/SetupView.swift +++ b/Sources/Secretive/Views/SetupView.swift @@ -50,12 +50,12 @@ struct SetupView: View { } Divider() StepView( - title: "Configure Integrations", - description: "Tell the tools you use how to talk to Secretive.", + title: .onboardingIntegrationsTitle, + description: LocalizedStringResource.onboardingIntegrationsDescription, systemImage: "firewall", ) { OnboardingButton( - "Configure", + LocalizedStringResource.onboardingIntegrationsButtonTitle, complete: integrations, width: buttonWidth ) { From 74ddb9595b474d6d777c548f386367324c6d6a2c Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Mon, 1 Sep 2025 19:31:16 -0700 Subject: [PATCH 15/17] WIP --- Sources/Packages/Localizable.xcstrings | 143 +++++++++++------- Sources/Secretive/App.swift | 2 +- .../Views/ConfigurationItemView.swift | 4 +- .../Secretive/Views/IntegrationsView.swift | 14 +- Sources/Secretive/Views/SetupView.swift | 4 +- 5 files changed, 97 insertions(+), 70 deletions(-) diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index 7022a66..f8b9d5b 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -1274,7 +1274,8 @@ } } }, - "Copy" : { + "copy_button" : { + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -2605,14 +2606,7 @@ } }, "Done" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Done" - } - } - } + }, "edit_cancel_button" : { "extractionState" : "manual", @@ -3141,26 +3135,6 @@ } } }, - "Getting Started" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Getting Started" - } - } - } - }, - "Integrations" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Integrations" - } - } - } - }, "integrations_add_this_title" : { "extractionState" : "manual", "localizations" : { @@ -3205,6 +3179,50 @@ } } }, + "integrations_getting_started_row_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Getting Started" + } + } + } + }, + "integrations_getting_started_section_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Integrations" + } + } + } + }, + "integrations_other_section_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Other" + } + } + } + }, + "integrations_other_shell_row_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "other" + } + } + } + }, "integrations_path_title" : { "extractionState" : "manual", "localizations" : { @@ -3216,6 +3234,28 @@ } } }, + "integrations_shell_section_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shell" + } + } + } + }, + "integrations_system_section_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "System" + } + } + } + }, "integrations_view_other_github_link" : { "extractionState" : "manual", "localizations" : { @@ -3238,7 +3278,8 @@ } } }, - "Integrations..." : { + "integrationsMenuBarTitle" : { + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -3479,6 +3520,17 @@ } } }, + "onboarding_done_button" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Done" + } + } + } + }, "onboarding_getting_started_multiple_config" : { "extractionState" : "manual", "localizations" : { @@ -3567,7 +3619,7 @@ } } }, - "onboarding_integrations_button_title" : { + "onboarding_integrations_button" : { "extractionState" : "manual", "localizations" : { "en" : { @@ -3599,12 +3651,6 @@ } } } - }, - "other" : { - - }, - "Other" : { - }, "persist_authentication_accept_button" : { "comment" : "When the user authorizes an action using a secret that requires unlock, they're shown a notification offering to leave the secret unlocked for a set period of time. This is the title for the notification.", @@ -3762,7 +3808,8 @@ } } }, - "Reveal in Finder" : { + "reveal_in_finder_button" : { + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -5350,16 +5397,6 @@ } } }, - "Shell" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Shell" - } - } - } - }, "signed_notification_description" : { "comment" : "When the user performs an action using a secret, they're shown a notification describing what happened. This is the description, showing which secret was used. The placeholder is the name of the secret.", "extractionState" : "manual", @@ -5587,16 +5624,6 @@ } } }, - "System" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "System" - } - } - } - }, "unnamed_secret" : { "extractionState" : "manual", "localizations" : { diff --git a/Sources/Secretive/App.swift b/Sources/Secretive/App.swift index 7e6c83d..b2db3af 100644 --- a/Sources/Secretive/App.swift +++ b/Sources/Secretive/App.swift @@ -65,7 +65,7 @@ struct Secretive: App { } .commands { CommandGroup(before: CommandGroupPlacement.appSettings) { - Button("Integrations...", systemImage: "app.connected.to.app.below.fill") { + Button(.integrationsMenuBarTitle, systemImage: "app.connected.to.app.below.fill") { showingIntegrations = true } } diff --git a/Sources/Secretive/Views/ConfigurationItemView.swift b/Sources/Secretive/Views/ConfigurationItemView.swift index 7544454..312182b 100644 --- a/Sources/Secretive/Views/ConfigurationItemView.swift +++ b/Sources/Secretive/Views/ConfigurationItemView.swift @@ -32,14 +32,14 @@ struct ConfigurationItemView: View { Spacer() switch action { case .copy(let string): - Button("Copy", systemImage: "document.on.document") { + Button(.copyButton, systemImage: "document.on.document") { NSPasteboard.general.declareTypes([.string], owner: nil) NSPasteboard.general.setString(string, forType: .string) } .labelStyle(.iconOnly) .buttonStyle(.borderless) case .revealInFinder(let rawPath): - Button("Reveal in Finder", systemImage: "folder") { + Button(.revealInFinderButton, systemImage: "folder") { // All foundation-based normalization methods replace this with the container directly. let processedPath = rawPath.replacingOccurrences(of: "~", with: "/Users/\(NSUserName())") let url = URL(filePath: processedPath) diff --git a/Sources/Secretive/Views/IntegrationsView.swift b/Sources/Secretive/Views/IntegrationsView.swift index 01be401..1e07ec1 100644 --- a/Sources/Secretive/Views/IntegrationsView.swift +++ b/Sources/Secretive/Views/IntegrationsView.swift @@ -23,7 +23,7 @@ struct IntegrationsView: View { } detail: { IntegrationsDetailView(selectedInstruction: $selectedInstruction) .fauxToolbar { - Button("Done") { + Button(.onboardingDoneButton) { dismiss() } .normalButton() @@ -190,7 +190,7 @@ private struct Instructions { zsh } - var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions("Getting Started", id: .gettingStarted) + var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions(LocalizedStringResource.integrationsGettingStartedRowTitle, id: .gettingStarted) var ssh: ConfigurationFileInstructions { ConfigurationFileInstructions( @@ -243,17 +243,17 @@ private struct Instructions { var instructions: [ConfigurationGroup] { [ - ConfigurationGroup(name: "Integrations", instructions: [ + ConfigurationGroup(name: LocalizedStringResource.integrationsGettingStartedSectionTitle, instructions: [ gettingStarted ]), ConfigurationGroup( - name: "System", + name: LocalizedStringResource.integrationsSystemSectionTitle, instructions: [ ssh, git, ] ), - ConfigurationGroup(name: "Shell", instructions: [ + ConfigurationGroup(name: LocalizedStringResource.integrationsShellSectionTitle, instructions: [ zsh, ConfigurationFileInstructions( tool: "bash", @@ -265,9 +265,9 @@ private struct Instructions { configPath: "~/.config/fish/config.fish", configText: "set -x SSH_AUTH_SOCK \(socketPath)" ), - ConfigurationFileInstructions("other", id: .otherShell), + ConfigurationFileInstructions(LocalizedStringResource.integrationsOtherShellRowTitle, id: .otherShell), ]), - ConfigurationGroup(name: "Other", instructions: [ + ConfigurationGroup(name: LocalizedStringResource.integrationsOtherSectionTitle, instructions: [ ConfigurationFileInstructions(LocalizedStringResource.integrationsAppsRowTitle, id: .otherApp), ]), ] diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift index ad26638..5e52ebf 100644 --- a/Sources/Secretive/Views/SetupView.swift +++ b/Sources/Secretive/Views/SetupView.swift @@ -55,7 +55,7 @@ struct SetupView: View { systemImage: "firewall", ) { OnboardingButton( - LocalizedStringResource.onboardingIntegrationsButtonTitle, + LocalizedStringResource.onboardingIntegrationsButton, complete: integrations, width: buttonWidth ) { @@ -70,7 +70,7 @@ struct SetupView: View { .frame(minWidth: 700, maxWidth: .infinity) HStack { Spacer() - Button("Done") { + Button(.onboardingDoneButton) { setupComplete = true dismiss() } From df2b7881c4c54f0dae23644ae817a85278587aa6 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Mon, 1 Sep 2025 19:37:59 -0700 Subject: [PATCH 16/17] WIP --- Sources/Packages/Localizable.xcstrings | 280 +++++++++--------- Sources/Secretive/Views/AgentStatusView.swift | 8 +- .../Secretive/Views/IntegrationsView.swift | 36 +-- Sources/Secretive/Views/SetupView.swift | 32 +- 4 files changed, 182 insertions(+), 174 deletions(-) diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index f8b9d5b..3b0a602 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -2604,9 +2604,6 @@ } } } - }, - "Done" : { - }, "edit_cancel_button" : { "extractionState" : "manual", @@ -3179,6 +3176,17 @@ } } }, + "integrations_getting_started_multiple_config" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You can configure more than one tool, they generally won't interfere with each other." + } + } + } + }, "integrations_getting_started_row_title" : { "extractionState" : "manual", "localizations" : { @@ -3201,6 +3209,83 @@ } } }, + "integrations_getting_started_suggestion_git" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you're trying to sign your git commits, set up Git Signing." + } + } + } + }, + "integrations_getting_started_suggestion_shell" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you're trying to configure anything your command line runs to use Secretive, configure your shell." + } + } + } + }, + "integrations_getting_started_suggestion_shell_default" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you don't known what shell you use and haven't changed it, you're probably using `%(shellName)@`." + } + } + } + }, + "integrations_getting_started_suggestion_ssh" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client." + } + } + } + }, + "integrations_getting_started_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuring Tools for Secretive" + } + } + } + }, + "integrations_getting_started_title_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Most tools will try and look for SSH keys on disk in `~/.ssh`. To use Secretive, we need to configure those tools to talk to Secretive instead." + } + } + } + }, + "integrations_getting_started_what_should_i_configure_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "What Should I Configure?" + } + } + } + }, "integrations_other_section_title" : { "extractionState" : "manual", "localizations" : { @@ -3520,138 +3605,6 @@ } } }, - "onboarding_done_button" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Done" - } - } - } - }, - "onboarding_getting_started_multiple_config" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "You can configure more than one tool, they generally won't interfere with each other." - } - } - } - }, - "onboarding_getting_started_suggestion_git" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "If you're trying to sign your git commits, set up Git Signing." - } - } - } - }, - "onboarding_getting_started_suggestion_shell" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "If you're trying to configure anything your command line runs to use Secretive, configure your shell." - } - } - } - }, - "onboarding_getting_started_suggestion_shell_default" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "If you don't known what shell you use and haven't changed it, you're probably using `%(shellName)@`." - } - } - } - }, - "onboarding_getting_started_suggestion_ssh" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "If you're trying to authenticate with an SSH server or authenticating with a service like GitHub over SSH, configure your SSH client." - } - } - } - }, - "onboarding_getting_started_title" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configuring Tools for Secretive" - } - } - } - }, - "onboarding_getting_started_title_description" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Most tools will try and look for SSH keys on disk in `~/.ssh`. To use Secretive, we need to configure those tools to talk to Secretive instead." - } - } - } - }, - "onboarding_getting_started_what_should_i_configure_title" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "What Should I Configure?" - } - } - } - }, - "onboarding_integrations_button" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configure" - } - } - } - }, - "onboarding_integrations_description" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tell the tools you use how to talk to Secretive." - } - } - } - }, - "onboarding_integrations_title" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configure Integrations" - } - } - } - }, "persist_authentication_accept_button" : { "comment" : "When the user authorizes an action using a secret that requires unlock, they're shown a notification offering to leave the secret unlocked for a set period of time. This is the title for the notification.", "extractionState" : "manual", @@ -4610,6 +4563,50 @@ } } }, + "setup_done_button" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Done" + } + } + } + }, + "setup_integrations_button" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configure" + } + } + } + }, + "setup_integrations_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tell the tools you use how to talk to Secretive." + } + } + } + }, + "setup_integrations_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configure Integrations" + } + } + } + }, "setup_ssh_add_for_me_button" : { "extractionState" : "manual", "localizations" : { @@ -5184,7 +5181,7 @@ } } }, - "setup_updates_ok" : { + "setup_updates_ok_button" : { "extractionState" : "manual", "localizations" : { "ca" : { @@ -5397,6 +5394,17 @@ } } }, + "setupStepCompleteButton" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Done" + } + } + } + }, "signed_notification_description" : { "comment" : "When the user performs an action using a secret, they're shown a notification describing what happened. This is the description, showing which secret was used. The placeholder is the name of the secret.", "extractionState" : "manual", diff --git a/Sources/Secretive/Views/AgentStatusView.swift b/Sources/Secretive/Views/AgentStatusView.swift index 082eccf..d89fca9 100644 --- a/Sources/Secretive/Views/AgentStatusView.swift +++ b/Sources/Secretive/Views/AgentStatusView.swift @@ -22,22 +22,22 @@ struct AgentRunningView: View { Section { if let process = agentStatusChecker.process { ConfigurationItemView( - title: LocalizedStringResource.agentDetailsLocationTitle, + title: .agentDetailsLocationTitle, value: process.bundleURL!.path(), action: .revealInFinder(process.bundleURL!.path()), ) ConfigurationItemView( - title: LocalizedStringResource.agentDetailsSocketPathTitle, + title: .agentDetailsSocketPathTitle, value: socketPath, action: .copy(socketPath), ) ConfigurationItemView( - title: LocalizedStringResource.agentDetailsVersionTitle, + title: .agentDetailsVersionTitle, value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String ) if let launchDate = process.launchDate { ConfigurationItemView( - title: LocalizedStringResource.agentDetailsRunningSinceTitle, + title: .agentDetailsRunningSinceTitle, value: launchDate.formatted() ) } diff --git a/Sources/Secretive/Views/IntegrationsView.swift b/Sources/Secretive/Views/IntegrationsView.swift index 1e07ec1..79781dc 100644 --- a/Sources/Secretive/Views/IntegrationsView.swift +++ b/Sources/Secretive/Views/IntegrationsView.swift @@ -23,7 +23,7 @@ struct IntegrationsView: View { } detail: { IntegrationsDetailView(selectedInstruction: $selectedInstruction) .fauxToolbar { - Button(.onboardingDoneButton) { + Button(.setupDoneButton) { dismiss() } .normalButton() @@ -80,24 +80,24 @@ struct IntegrationsDetailView: View { switch selectedInstruction.id { case .gettingStarted: Form { - Section(.onboardingGettingStartedTitle) { - Text(.onboardingGettingStartedTitleDescription) + Section(.integrationsGettingStartedTitle) { + Text(.integrationsGettingStartedTitleDescription) } Section { Group { - Text(.onboardingGettingStartedSuggestionSsh) + Text(.integrationsGettingStartedSuggestionSsh) .onTapGesture { self.selectedInstruction = instructions.ssh } VStack(alignment: .leading, spacing: 5) { - Text(.onboardingGettingStartedSuggestionShell) - Text(.onboardingGettingStartedSuggestionShellDefault(shellName: instructions.defaultShell.tool)) + Text(.integrationsGettingStartedSuggestionShell) + Text(.integrationsGettingStartedSuggestionShellDefault(shellName: instructions.defaultShell.tool)) .font(.caption2) } .onTapGesture { self.selectedInstruction = instructions.defaultShell } - Text(.onboardingGettingStartedSuggestionGit) + Text(.integrationsGettingStartedSuggestionGit) .onTapGesture { self.selectedInstruction = instructions.git } @@ -105,10 +105,10 @@ struct IntegrationsDetailView: View { .foregroundStyle(.link) } header: { - Text(.onboardingGettingStartedWhatShouldIConfigureTitle) + Text(.integrationsGettingStartedWhatShouldIConfigureTitle) } footer: { - Text(.onboardingGettingStartedMultipleConfig) + Text(.integrationsGettingStartedMultipleConfig) } } .formStyle(.grouped) @@ -116,9 +116,9 @@ struct IntegrationsDetailView: View { Form { ForEach(selectedInstruction.steps) { stepGroup in Section { - ConfigurationItemView(title: LocalizedStringResource.integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path)) + ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path)) ForEach(stepGroup.steps, id: \.self) { step in - ConfigurationItemView(title: LocalizedStringResource.integrationsAddThisTitle, action: .copy(step)) { + ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(step)) { HStack { Text(step) .padding(8) @@ -190,7 +190,7 @@ private struct Instructions { zsh } - var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions(LocalizedStringResource.integrationsGettingStartedRowTitle, id: .gettingStarted) + var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions(.integrationsGettingStartedRowTitle, id: .gettingStarted) var ssh: ConfigurationFileInstructions { ConfigurationFileInstructions( @@ -243,17 +243,17 @@ private struct Instructions { var instructions: [ConfigurationGroup] { [ - ConfigurationGroup(name: LocalizedStringResource.integrationsGettingStartedSectionTitle, instructions: [ + ConfigurationGroup(name: .integrationsGettingStartedSectionTitle, instructions: [ gettingStarted ]), ConfigurationGroup( - name: LocalizedStringResource.integrationsSystemSectionTitle, + name: .integrationsSystemSectionTitle, instructions: [ ssh, git, ] ), - ConfigurationGroup(name: LocalizedStringResource.integrationsShellSectionTitle, instructions: [ + ConfigurationGroup(name: .integrationsShellSectionTitle, instructions: [ zsh, ConfigurationFileInstructions( tool: "bash", @@ -265,10 +265,10 @@ private struct Instructions { configPath: "~/.config/fish/config.fish", configText: "set -x SSH_AUTH_SOCK \(socketPath)" ), - ConfigurationFileInstructions(LocalizedStringResource.integrationsOtherShellRowTitle, id: .otherShell), + ConfigurationFileInstructions(.integrationsOtherShellRowTitle, id: .otherShell), ]), - ConfigurationGroup(name: LocalizedStringResource.integrationsOtherSectionTitle, instructions: [ - ConfigurationFileInstructions(LocalizedStringResource.integrationsAppsRowTitle, id: .otherApp), + ConfigurationGroup(name: .integrationsOtherSectionTitle, instructions: [ + ConfigurationFileInstructions(.integrationsAppsRowTitle, id: .otherApp), ]), ] } diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift index 5e52ebf..9a49930 100644 --- a/Sources/Secretive/Views/SetupView.swift +++ b/Sources/Secretive/Views/SetupView.swift @@ -19,12 +19,12 @@ struct SetupView: View { VStack { VStack(alignment: .leading, spacing: 0) { StepView( - title: "setup_agent_title", - description: "setup_agent_description", + title: .setupAgentTitle, + description: .setupAgentDescription, systemImage: "lock.laptopcomputer", ) { - OnboardingButton( - "setup_agent_install_button", + setupButton( + .setupAgentInstallButton, complete: installed, width: buttonWidth ) { @@ -36,12 +36,12 @@ struct SetupView: View { } Divider() StepView( - title: "setup_updates_title", - description: "setup_updates_description", + title: .setupUpdatesTitle, + description: .setupUpdatesDescription, systemImage: "network.badge.shield.half.filled", ) { - OnboardingButton( - "setup_updates_ok", + setupButton( + .setupUpdatesOkButton, complete: updates, width: buttonWidth ) { @@ -50,12 +50,12 @@ struct SetupView: View { } Divider() StepView( - title: .onboardingIntegrationsTitle, - description: LocalizedStringResource.onboardingIntegrationsDescription, + title: .setupIntegrationsTitle, + description: .setupIntegrationsDescription, systemImage: "firewall", ) { - OnboardingButton( - LocalizedStringResource.onboardingIntegrationsButton, + setupButton( + .setupIntegrationsButton, complete: integrations, width: buttonWidth ) { @@ -63,14 +63,14 @@ struct SetupView: View { } } } - .onPreferenceChange(OnboardingButton.WidthKey.self) { width in + .onPreferenceChange(setupButton.WidthKey.self) { width in buttonWidth = width } .background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10)) .frame(minWidth: 700, maxWidth: .infinity) HStack { Spacer() - Button(.onboardingDoneButton) { + Button(.setupDoneButton) { setupComplete = true dismiss() } @@ -88,7 +88,7 @@ struct SetupView: View { } } -struct OnboardingButton: View { +struct setupButton: View { struct WidthKey: @MainActor PreferenceKey { @MainActor static var defaultValue: CGFloat? = nil @@ -117,7 +117,7 @@ struct OnboardingButton: View { Button(action: action) { HStack(spacing: 6) { if complete { - Text("Done") + Text(.setupStepCompleteButton) Image(systemName: "checkmark.circle.fill") } else { Text(label) From ea71993801b2db017f9c4b8c7fb785a34276adc1 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Mon, 1 Sep 2025 20:07:58 -0700 Subject: [PATCH 17/17] WIP --- Sources/Packages/Localizable.xcstrings | 432 ------------------ .../Views/ConfigurationItemView.swift | 2 +- Sources/Secretive/Views/CopyableView.swift | 8 +- 3 files changed, 5 insertions(+), 437 deletions(-) diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index 3b0a602..f959af0 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -4607,154 +4607,6 @@ } } }, - "setup_ssh_add_for_me_button" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afegeix-ho per mi" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Für Mich Einfügen" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Add it For Me" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ajoutez-le pour moi" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aggiungila per me" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "自動で追加する" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "나를 위해 추가해주세요" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dodaj za mnie" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adicionar para mim" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добавить для меня" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "为我添加" - } - } - } - }, - "setup_ssh_add_to_config_button" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afegeix a %1$(configPath)@" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "In %1$(configPath)@ einfügen" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Add to %1$(configPath)@" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Add to %1$(configPath)@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ajouter à %1$(configPath)@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aggiungi a %1$(configPath)@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$(configPath)@に追加" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$(configPath)@에 추가" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dodaj do %1$(configPath)@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adicionar para %1$(configPath)@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добавить к %1$(configPath)@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "添加到 %1$(configPath)@" - } - } - } - }, "setup_ssh_added_manually_button" : { "extractionState" : "manual", "localizations" : { @@ -4826,290 +4678,6 @@ } } }, - "setup_ssh_description" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afegeix aquesta línia a la teua configuració del shell per que SSH es comunique amb Secretive quan vulga autenticar. Secretive pot fer aquest procediment automàticament, o pots copiar i pegar açò al teu fitxer de configuració." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Füge diese Zeile in deine Shell-Konfiguration ein, damit SSH zur Authentifizierung mit dem Secret Agent kommuniziert. Secretive kann dies automatisch tun, oder du kopierst diese Zeile in deine Konfigurationsdatei." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Add this line to your shell config telling SSH to talk to Secret Agent when it wants to authenticate. Secretive can either do this for you automatically, or you can copy and paste this into your config file." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ajoutez cette ligne à votre configuration shell pour indiquer à SSH de communiquer à Secret Agent quand il veut s'authentifier. Secretive peut le faire automatiquement pour vous, ou vous pouvez copier et coller cette ligne dans votre fichier de configuration." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aggiungi questa riga alla configurazione del Terminale per dire a SSH di parlare con Secret Agent quando vuole autenticarsi. Secretive può farlo automaticamente per te, oppure puoi copiare e incollare questa riga nel file di configurazione." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "以下の行をシェルの設定に追加してSSHが認証の際にSecretAgentを利用できるようにしてください。Secretiveが自動で追加するか、手動でコピーして設定に追加することもできます。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "SSH가 인증을 원할 때 Secret Agent와 통신하도록 지시하는 이 줄을 쉘 구성에 추가하세요. Secretive는 이 작업을 자동으로 수행하거나 사용자가 이를 복사하여 구성 파일에 붙여넣을 수 있습니다." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dodaj tą linijkę to pliku konfiguracyjnego SSH, aby nawiązać połączenie z Secret Agent kiedy potrzebna jest autoryzacja. Secretive może ustawić to automatycznie lub możesz to zrobić samodzielnie kopiując to do pliku konfiguracyjnego." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adicione esta linha nas configurações do seu shell para dizer ao SSH para falar com o Secret Agent quando ele necessitar de autenticação. Secretive pode fazer isto para você automaticamente ou você pode copiar e colar isso no seu arquivo de configuração." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добавьте эту строчку к вашему конфигу shell, так SSH будет использовать SecretAgent в процессе аутентификации. Secretive может сделать это за Вас, либо Вы можете это скопировать сами." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "将以下文本添加到您的SSH 配置中以使用Secret Agent. Secretive 无法自动帮您完成该过程,或者您可以选择拷贝并粘贴该文本到您的配置文件中" - } - } - } - }, - "setup_ssh_title" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configura el teu agent SSH" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Konfiguriere deinen SSH Agent" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configure your SSH Agent" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurer votre Agent SSH" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configura il tuo Agente SSH" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "SSHエージェントを設定" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "SSH Agent 설정" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skonfiguruj twojego klienta SSH" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurar seu agente SSH" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Настройте Ваш SSH Agent" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "设置您的SSH 代理" - } - } - } - }, - "setup_step_complete_symbol" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - } - } - }, - "setup_third_party_faq_link" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Si tractes de configurar una aplicació de tercers, comprova el FAQ." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schaue dir die FAQs an, um eine Drittanbieter-App einzurichten." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "If you're trying to set up a third party app, check out the FAQ." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Si vous essayez de configurer une application tierce, consultez la FAQ." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se stai cercando di impostare un’app di terze parti, dai un'occhiata alla FAQ." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "その他のアプリから使う場合はよくある質問をご覧ください。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "타사 앱을 설정하려는 경우 FAQ를 확인하세요." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jeżeli próbujesz ustawić aplikacje stron trzecich, sprawdź FAQ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se você estiver tentando configurar um aplicativo de terceiros, verifique o FAQ." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Если Вы пытаетесь настроить сторонее приложение, ознакомьтесь с FAQ." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "如果您想设置第三方APP,请阅读 FAQ。" - } - } - } - }, "setup_updates_description" : { "extractionState" : "manual", "localizations" : { diff --git a/Sources/Secretive/Views/ConfigurationItemView.swift b/Sources/Secretive/Views/ConfigurationItemView.swift index 312182b..2c8520b 100644 --- a/Sources/Secretive/Views/ConfigurationItemView.swift +++ b/Sources/Secretive/Views/ConfigurationItemView.swift @@ -32,7 +32,7 @@ struct ConfigurationItemView: View { Spacer() switch action { case .copy(let string): - Button(.copyButton, systemImage: "document.on.document") { + Button(.copyableClickToCopyButton, systemImage: "document.on.document") { NSPasteboard.general.declareTypes([.string], owner: nil) NSPasteboard.general.setString(string, forType: .string) } diff --git a/Sources/Secretive/Views/CopyableView.swift b/Sources/Secretive/Views/CopyableView.swift index d1be4be..23855f6 100644 --- a/Sources/Secretive/Views/CopyableView.swift +++ b/Sources/Secretive/Views/CopyableView.swift @@ -76,10 +76,10 @@ struct CopyableView: View { switch interactionState { case .hovering: Image(systemName: "document.on.document") - .accessibilityLabel(String(localized: "copyable_click_to_copy_button")) + .accessibilityLabel(String(localized: .copyableClickToCopyButton)) case .clicking: Image(systemName: "checkmark.circle.fill") - .accessibilityLabel(String(localized: "copyable_copied")) + .accessibilityLabel(String(localized: .copyableCopied)) case .normal, .dragging: EmptyView() } @@ -168,9 +168,9 @@ fileprivate struct BackgroundViewModifier: ViewModifier { struct CopyableView_Previews: PreviewProvider { static var previews: some View { Group { - CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Hello world.") + CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.") .padding() - CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ") + CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ") .padding() } }