From c42bfe38a39cf479dfebe2e5c8d50ebfb114e066 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Tue, 2 Sep 2025 22:56:31 -0700 Subject: [PATCH] WIP --- Sources/Packages/Localizable.xcstrings | 172 +++++++++++++++--- Sources/Packages/Package.swift | 1 + .../Packages/Sources/Localization/Stub.swift | 1 - .../PublicKeyStandinFileController.swift | 4 + Sources/Secretive.xcodeproj/project.pbxproj | 4 + Sources/Secretive/Controllers/URLs.swift | 12 ++ Sources/Secretive/Views/AgentStatusView.swift | 5 +- .../Secretive/Views/IntegrationsView.swift | 78 ++++---- .../Secretive/Views/SecretDetailView.swift | 8 - 9 files changed, 209 insertions(+), 76 deletions(-) delete mode 100644 Sources/Packages/Sources/Localization/Stub.swift create mode 100644 Sources/Secretive/Controllers/URLs.swift diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index ac18f3d..4748494 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -2983,73 +2983,73 @@ "localizations" : { "ca" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive suporta claus EC256, EC384, RSA1024 i RSA2048." } }, "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive unterstützt EC256, EC384, RSA1024 und RSA2048 Schlüssel." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Secretive supports EC256, EC384, RSA1024, and RSA2048 keys." + "value" : "Secretive supports EC256, EC384, and RSA2048 keys." } }, "fi" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive tukee EC256-, EC384-, RSA1024- ja RSA2048-avaimia." } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive prend en charge les clés EC256, EC384, RSA1024 et RSA2048." } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive supporta la cifratura EC256, EC384, RSA1024 e RSA2048." } }, "ja" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "SecretiveはEC256、EC384、RSA1024、またはRSA2048の鍵に対応しています。" } }, "ko" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive는 EC256, EC384, RSA1024 및 RSA2048 키를 지원합니다." } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive wspiera klucze EC256, EC384, RSA1024 i RSA2048." } }, "pt-BR" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive suporta chaves EC256, EC384, RSA1024 e RSA2048." } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive поддерживает ключи EC256, EC384, RSA1024, и RSA2048." } }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Secretive 支持 EC256, EC384, RSA1024, 和RSA2048." } } @@ -3132,6 +3132,12 @@ } } }, + "export SSH_AUTH_SOCK=%@" : { + "shouldTranslate" : false + }, + "Host *\n\tIdentityAgent %@" : { + "shouldTranslate" : false + }, "integrations_add_this_title" : { "extractionState" : "manual", "localizations" : { @@ -3286,6 +3292,40 @@ } } }, + "integrations_git_step_gitallowedsigners_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "~/.gitallowedsigners probably does not exist. You'll need to create it." + } + } + } + }, + "integrations_git_step_gitconfig_description" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "[user]\n signingkey = %1$(publicKeyPathPlaceholder)@\n[commit]\n gpgsign = true\n[gpg]\n format = ssh\n[gpg \"ssh\"]\n allowedSignersFile = ~/.gitallowedsigners" + } + } + }, + "shouldTranslate" : false + }, + "integrations_menu_bar_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Integrations…" + } + } + } + }, "integrations_other_section_title" : { "extractionState" : "manual", "localizations" : { @@ -3319,6 +3359,28 @@ } } }, + "integrations_public_key_path_placeholder" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "YOUR_PUBLIC_KEY_PATH" + } + } + } + }, + "integrations_public_key_placeholder" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "YOUR_PUBLIC_KEY" + } + } + } + }, "integrations_shell_section_title" : { "extractionState" : "manual", "localizations" : { @@ -3330,6 +3392,17 @@ } } }, + "integrations_ssh_specific_key_note" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You can tell SSH to use a specific key for a given host. See the web documentation for more details." + } + } + } + }, "integrations_system_section_title" : { "extractionState" : "manual", "localizations" : { @@ -3341,6 +3414,65 @@ } } }, + "integrations_tool_name_bash" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "bash" + } + } + }, + "shouldTranslate" : false + }, + "integrations_tool_name_fish" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "fish" + } + } + }, + "shouldTranslate" : false + }, + "integrations_tool_name_git_signing" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Git Signing" + } + } + } + }, + "integrations_tool_name_ssh" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH" + } + } + }, + "shouldTranslate" : false + }, + "integrations_tool_name_zsh" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "zsh" + } + } + }, + "shouldTranslate" : false + }, "integrations_view_other_github_link" : { "extractionState" : "manual", "localizations" : { @@ -3363,13 +3495,13 @@ } } }, - "integrationsMenuBarTitle" : { + "integrationsGitStepGitconfigSectionNote" : { "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Integrations…" + "value" : "If any section (like [user]) already exists, just add the entries in the existing section." } } } @@ -4269,16 +4401,8 @@ } } }, - "Setup" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Setup" - } - } - } + "set -x SSH_AUTH_SOCK %@" : { + "shouldTranslate" : false }, "setup_agent_activity_monitor_description" : { "extractionState" : "manual", diff --git a/Sources/Packages/Package.swift b/Sources/Packages/Package.swift index 82322b2..4e3fe45 100644 --- a/Sources/Packages/Package.swift +++ b/Sources/Packages/Package.swift @@ -83,6 +83,7 @@ let package = Package( var localization: Resource { .process("../../Localizable.xcstrings") +// .process("../../Resources/Localizable.xcstrings") } var swiftSettings: [PackageDescription.SwiftSetting] { diff --git a/Sources/Packages/Sources/Localization/Stub.swift b/Sources/Packages/Sources/Localization/Stub.swift deleted file mode 100644 index 8b13789..0000000 --- a/Sources/Packages/Sources/Localization/Stub.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift b/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift index 71bb426..b880584 100644 --- a/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift +++ b/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift @@ -47,6 +47,10 @@ public final class PublicKeyFileStoreController: Sendable { return directory.appending(component: "\(minimalHex).pub").path() } + public func publicKeyPath(for name: String) -> String { + return directory.appending(component: "\(name).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 { diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index 029fe62..fc7a0a3 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; }; 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; }; 5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; }; + 504788EC2E680DC800B4556F /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788EB2E680DC400B4556F /* URLs.swift */; }; 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; }; 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; }; 50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; }; @@ -110,6 +111,7 @@ 50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = ""; }; 50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = ""; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = ""; }; + 504788EB2E680DC400B4556F /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = ""; }; 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = ""; }; 50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = ""; }; 50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -272,6 +274,7 @@ 508A58B1241ED1EA0069DC07 /* Controllers */ = { isa = PBXGroup; children = ( + 504788EB2E680DC400B4556F /* URLs.swift */, 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */, 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, @@ -445,6 +448,7 @@ 50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */, 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */, 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */, + 504788EC2E680DC800B4556F /* URLs.swift in Sources */, 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */, 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */, diff --git a/Sources/Secretive/Controllers/URLs.swift b/Sources/Secretive/Controllers/URLs.swift new file mode 100644 index 0000000..7b63ea1 --- /dev/null +++ b/Sources/Secretive/Controllers/URLs.swift @@ -0,0 +1,12 @@ +import Foundation + +extension URL { + + static var agentHomeURL: URL { + URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID)) + } + + static var socketPath: String { + URL.agentHomeURL.appendingPathComponent("socket.ssh").path() + } +} diff --git a/Sources/Secretive/Views/AgentStatusView.swift b/Sources/Secretive/Views/AgentStatusView.swift index d89fca9..28139fe 100644 --- a/Sources/Secretive/Views/AgentStatusView.swift +++ b/Sources/Secretive/Views/AgentStatusView.swift @@ -15,7 +15,6 @@ struct AgentStatusView: View { 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 { @@ -28,8 +27,8 @@ struct AgentRunningView: View { ) ConfigurationItemView( title: .agentDetailsSocketPathTitle, - value: socketPath, - action: .copy(socketPath), + value: URL.socketPath, + action: .copy(URL.socketPath), ) ConfigurationItemView( title: .agentDetailsVersionTitle, diff --git a/Sources/Secretive/Views/IntegrationsView.swift b/Sources/Secretive/Views/IntegrationsView.swift index 79781dc..2b8e36d 100644 --- a/Sources/Secretive/Views/IntegrationsView.swift +++ b/Sources/Secretive/Views/IntegrationsView.swift @@ -1,4 +1,5 @@ import SwiftUI +import SecretKit struct IntegrationsView: View { @@ -91,7 +92,7 @@ struct IntegrationsDetailView: View { } VStack(alignment: .leading, spacing: 5) { Text(.integrationsGettingStartedSuggestionShell) - Text(.integrationsGettingStartedSuggestionShellDefault(shellName: instructions.defaultShell.tool)) + Text(.integrationsGettingStartedSuggestionShellDefault(shellName: String(localized: instructions.defaultShell.tool))) .font(.caption2) } .onTapGesture { @@ -117,8 +118,8 @@ struct IntegrationsDetailView: View { ForEach(selectedInstruction.steps) { stepGroup in Section { ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path)) - ForEach(stepGroup.steps, id: \.self) { step in - ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(step)) { + ForEach(stepGroup.steps, id: \.self.key) { step in + ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(String(localized: step))) { HStack { Text(step) .padding(8) @@ -183,50 +184,39 @@ struct IntegrationsDetailView: View { private struct Instructions { - private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String - + private let publicKeyPath = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL).publicKeyPath(for: String(localized: .integrationsPublicKeyPathPlaceholder)) var defaultShell: ConfigurationFileInstructions { zsh } - var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions(.integrationsGettingStartedRowTitle, id: .gettingStarted) + var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions(.integrationsGettingStartedRowTitle, id: .gettingStarted) var ssh: ConfigurationFileInstructions { ConfigurationFileInstructions( - tool: "SSH", + tool: LocalizedStringResource.integrationsToolNameSsh, configPath: "~/.ssh/config", - configText: "Host *\n\tIdentityAgent \(socketPath)", + configText: "Host *\n\tIdentityAgent \(URL.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.", + note: .integrationsSshSpecificKeyNote, ) } var git: ConfigurationFileInstructions { ConfigurationFileInstructions( - tool: "Git Signing", + tool: .integrationsToolNameGitSigning, steps: [ .init(path: "~/.gitconfig", steps: [ - """ - [user] - signingkey = YOUR_PUBLIC_KEY_PATH - [commit] - gpgsign = true - [gpg] - format = ssh - [gpg "ssh"] - allowedSignersFile = ~/.gitallowedsigners - """ + .integrationsGitStepGitconfigDescription(publicKeyPathPlaceholder: publicKeyPath) ], - note: "If any section (like [user]) already exists, just add the entries in the existing section." - - ), + note: .integrationsGitStepGitconfigSectionNote + ), .init( path: "~/.gitallowedsigners", steps: [ - "YOUR_PUBLIC_KEY" + .integrationsPublicKeyPlaceholder ], - note: "~/.gitallowedsigners probably does not exist. You'll need to create it." + note: .integrationsGitStepGitallowedsignersDescription ), ], website: URL(string: "https://git-scm.com/docs/git-config")!, @@ -235,9 +225,9 @@ private struct Instructions { var zsh: ConfigurationFileInstructions { ConfigurationFileInstructions( - tool: "zsh", + tool: .integrationsToolNameZsh, configPath: "~/.zshrc", - configText: "export SSH_AUTH_SOCK=\(socketPath)" + configText: "export SSH_AUTH_SOCK=\(URL.socketPath)" ) } @@ -256,14 +246,14 @@ private struct Instructions { ConfigurationGroup(name: .integrationsShellSectionTitle, instructions: [ zsh, ConfigurationFileInstructions( - tool: "bash", + tool: .integrationsToolNameBash, configPath: "~/.bashrc", - configText: "export SSH_AUTH_SOCK=\(socketPath)" + configText: "export SSH_AUTH_SOCK=\(URL.socketPath)" ), ConfigurationFileInstructions( - tool: "fish", + tool: .integrationsToolNameFish, configPath: "~/.config/fish/config.fish", - configText: "set -x SSH_AUTH_SOCK \(socketPath)" + configText: "set -x SSH_AUTH_SOCK \(URL.socketPath)" ), ConfigurationFileInstructions(.integrationsOtherShellRowTitle, id: .otherShell), ]), @@ -285,31 +275,35 @@ struct ConfigurationFileInstructions: Hashable, Identifiable { struct StepGroup: Hashable, Identifiable { let path: String - let steps: [String] - let note: String? + let steps: [LocalizedStringResource] + let note: LocalizedStringResource? var id: String { path } - init(path: String, steps: [String], note: String? = nil) { + init(path: String, steps: [LocalizedStringResource], note: LocalizedStringResource? = nil) { self.path = path self.steps = steps self.note = note } + + func hash(into hasher: inout Hasher) { + id.hash(into: &hasher) + } } var id: ID - var tool: String + var tool: LocalizedStringResource var steps: [StepGroup] var website: URL? - init(tool: String, configPath: String, configText: String, website: URL? = nil, note: String? = nil) { - self.id = .tool(tool) + init(tool: LocalizedStringResource, configPath: String, configText: LocalizedStringResource, website: URL? = nil, note: LocalizedStringResource? = nil) { + self.id = .tool(String(localized: tool)) self.tool = tool self.steps = [StepGroup(path: configPath, steps: [configText], note: note)] self.website = website } - init(tool: String, steps: [StepGroup], website: URL? = nil) { - self.id = .tool(tool) + init(tool: LocalizedStringResource, steps: [StepGroup], website: URL? = nil) { + self.id = .tool(String(localized: tool)) self.tool = tool self.steps = steps self.website = website @@ -317,10 +311,14 @@ struct ConfigurationFileInstructions: Hashable, Identifiable { init(_ name: LocalizedStringResource, id: ID) { self.id = id - tool = String(localized: name) + tool = name self.steps = [] } + func hash(into hasher: inout Hasher) { + id.hash(into: &hasher) + } + enum ID: Identifiable, Hashable { case gettingStarted case tool(String) diff --git a/Sources/Secretive/Views/SecretDetailView.swift b/Sources/Secretive/Views/SecretDetailView.swift index 69b6d33..d9081ac 100644 --- a/Sources/Secretive/Views/SecretDetailView.swift +++ b/Sources/Secretive/Views/SecretDetailView.swift @@ -37,14 +37,6 @@ struct SecretDetailView: View { } -extension URL { - - static var agentHomeURL: URL { - URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID)) - } - -} - #Preview { SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret")) }