Merge branch 'newsetup' into menubar

This commit is contained in:
Max Goedjen 2025-09-01 20:19:05 -07:00
commit c352ed4cc9
No known key found for this signature in database
24 changed files with 1506 additions and 869 deletions

View File

@ -1,6 +1,108 @@
{
"sourceLanguage" : "en",
"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",
"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",
"localizations" : {
@ -386,6 +488,17 @@
}
}
},
"agentDetailsLocationTitle" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Secret Agent Location"
}
}
}
},
"app_menu_help_button" : {
"extractionState" : "manual",
"localizations" : {
@ -1161,6 +1274,17 @@
}
}
},
"copy_button" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Copy"
}
}
}
},
"copyable_click_to_copy_button" : {
"extractionState" : "manual",
"localizations" : {
@ -1497,7 +1621,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. Its usually an email address."
}
}
}
@ -3008,6 +3132,248 @@
}
}
},
"integrations_add_this_title" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add This:"
}
}
}
},
"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_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" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Getting Started"
}
}
}
},
"integrations_getting_started_section_title" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Integrations"
}
}
}
},
"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" : {
"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" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Configuration File"
}
}
}
},
"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" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "View on GitHub"
}
}
}
},
"integrations_web_link" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "View Documentation on Web"
}
}
}
},
"integrationsMenuBarTitle" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Integrations…"
}
}
}
},
"no_secure_storage_description" : {
"extractionState" : "manual",
"localizations" : {
@ -3395,6 +3761,17 @@
}
}
},
"reveal_in_finder_button" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Reveal in Finder"
}
}
}
},
"secret_detail_md5_fingerprint_label" : {
"extractionState" : "manual",
"localizations" : {
@ -3892,6 +4269,16 @@
}
}
},
"Setup" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Setup"
}
}
}
},
"setup_agent_activity_monitor_description" : {
"extractionState" : "manual",
"localizations" : {
@ -4176,150 +4563,46 @@
}
}
},
"setup_ssh_add_for_me_button" : {
"setup_done_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" : "为我添加"
"value" : "Done"
}
}
}
},
"setup_ssh_add_to_config_button" : {
"setup_integrations_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)@"
"value" : "Configure"
}
},
"fi" : {
}
}
},
"setup_integrations_description" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add to %1$(configPath)@"
"value" : "Tell the tools you use how to talk to Secretive."
}
},
"fr" : {
}
}
},
"setup_integrations_title" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"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)@"
"value" : "Configure Integrations"
}
}
}
@ -4395,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 unapp 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" : {
@ -4750,7 +4749,7 @@
}
}
},
"setup_updates_ok" : {
"setup_updates_ok_button" : {
"extractionState" : "manual",
"localizations" : {
"ca" : {
@ -4963,6 +4962,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",

View File

@ -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)] = [:]

View File

@ -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<SecretType: Secret>(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<SecretType: Secret>(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()
}
}

View File

@ -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)
}()

View File

@ -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 */; };
@ -49,8 +48,12 @@
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 /* 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 */; };
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 */
@ -120,7 +123,6 @@
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = "<group>"; };
5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; };
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = "<group>"; };
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; };
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = "<group>"; };
@ -138,8 +140,12 @@
50A3B79624026B7600D209EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
50A3B79824026B7600D209EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50A3B79924026B7600D209EA /* SecretAgent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgent.entitlements; sourceTree = "<group>"; };
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationsView.swift; sourceTree = "<group>"; };
50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = "<group>"; };
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; };
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusView.swift; sourceTree = "<group>"; };
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorStyle.swift; sourceTree = "<group>"; };
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = "<group>"; };
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -254,7 +260,11 @@
506772C82425BB8500034DED /* NoStoresView.swift */,
50153E1F250AFCB200525160 /* UpdateView.swift */,
5066A6C12516F303004B5A36 /* SetupView.swift */,
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -266,7 +276,6 @@
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */,
);
path = Controllers;
sourceTree = "<group>";
@ -433,6 +442,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 */,
@ -442,16 +452,18 @@
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 */,
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */,
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 */,
@ -647,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)",
@ -679,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)",
@ -783,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)",
@ -809,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)",
@ -835,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)",
@ -862,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)",

View File

@ -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,16 @@ struct Secretive: App {
forceLaunchAgent()
}
}
.sheet(isPresented: $showingIntegrations) {
IntegrationsView()
}
}
.commands {
CommandGroup(before: CommandGroupPlacement.appSettings) {
Button(.integrationsMenuBarTitle, systemImage: "app.connected.to.app.below.fill") {
showingIntegrations = true
}
}
CommandGroup(after: CommandGroupPlacement.newItem) {
Button(.appMenuNewSecretButton) {
showingCreation = true
@ -72,7 +81,7 @@ struct Secretive: App {
}
}
CommandGroup(after: .help) {
Button(.appMenuSetupButton) {
Button("Setup") {
showingSetup = true
}
}
@ -87,7 +96,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 {

View File

@ -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,32 +22,39 @@ import Observation
}
func check() {
running = instanceSecretAgentProcess != nil
process = instanceSecretAgentProcess
running = process != nil
}
// 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) {
if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) || (url.isXcodeURL && developmentBuild) {
return agent
}
}
return nil
}
// 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")
}
}

View File

@ -8,16 +8,28 @@ 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)
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))
let result = await MainActor.run {
setEnabled(false)
}
try? await Task.sleep(for: .seconds(1))
return result
}
func forceLaunch() async -> Bool {
@ -28,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)")
@ -36,7 +49,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()

View File

@ -1,63 +0,0 @@
import Foundation
import Cocoa
import SecretKit
struct ShellConfigurationController {
let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.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
}
}

View File

@ -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")
}
}

View File

@ -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() {

View File

@ -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,8 +18,77 @@ struct PrimaryButtonModifier: ViewModifier {
extension View {
func primary() -> some View {
func primaryButton() -> some View {
modifier(PrimaryButtonModifier())
}
}
struct MenuButtonModifier: 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 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())
}
}
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())
}
}

View File

@ -0,0 +1,154 @@
import SwiftUI
struct AgentStatusView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
var body: some View {
if agentStatusChecker.running {
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 {
ConfigurationItemView(
title: .agentDetailsLocationTitle,
value: process.bundleURL!.path(),
action: .revealInFinder(process.bundleURL!.path()),
)
ConfigurationItemView(
title: .agentDetailsSocketPathTitle,
value: socketPath,
action: .copy(socketPath),
)
ConfigurationItemView(
title: .agentDetailsVersionTitle,
value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
)
if let launchDate = process.launchDate {
ConfigurationItemView(
title: .agentDetailsRunningSinceTitle,
value: launchDate.formatted()
)
}
}
} header: {
Text(.agentRunningNoticeDetailTitle)
.font(.headline)
.padding(.top)
} footer: {
VStack(alignment: .leading, spacing: 10) {
Text(.agentRunningNoticeDetailDescription)
HStack {
Spacer()
Menu(.agentDetailsRestartAgentButton) {
Button(.agentDetailsDisableAgentButton) {
Task {
_ = await LaunchAgentController()
.uninstall()
agentStatusChecker.check()
}
}
} primaryAction: {
Task {
let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
}
}
}
}
.padding(.vertical)
}
}
.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(.agentDetailsStartAgentButton)
} else {
HStack {
Text(.agentDetailsStartAgentButtonStarting)
ProgressView()
.controlSize(.mini)
}
}
}
.primaryButton()
} else {
Text(.agentDetailsCouldNotStartError)
.bold()
.foregroundStyle(.red)
}
}
}
.padding(.bottom)
}
}
.formStyle(.grouped)
.frame(width: 400)
}
}
#Preview {
AgentStatusView()
.environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
}
#Preview {
AgentStatusView()
.environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current))
}

View File

@ -0,0 +1,59 @@
import SwiftUI
struct ConfigurationItemView<Content: View>: 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(.copyableClickToCopyButton, 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(.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)
let folder = url.deletingLastPathComponent().path()
NSWorkspace.shared.selectFile(processedPath, inFileViewerRootedAtPath: folder)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
case nil:
EmptyView()
}
}
content
}
}
}

View File

@ -36,7 +36,7 @@ struct ContentView: View {
toolbarItem(newItemView, id: "new")
}
.sheet(isPresented: $runningSetup) {
SetupView(visible: $runningSetup, setupComplete: $hasRunSetup)
SetupView(setupComplete: $hasRunSetup)
}
}
@ -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
}
}
@ -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)
}
}
@ -103,18 +103,17 @@ extension ContentView {
@ViewBuilder
var newItemView: some View {
if storeList.modifiableStore?.isAvailable ?? false {
Button(action: {
Button(.appMenuNewSecretButton, systemImage: "plus") {
showingCreation = true
}, label: {
Image(systemName: "plus")
})
}
.menuButton()
.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) { created in
if let created {
activeSecret = created
}
}
}
}
}
@ -125,43 +124,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 +193,6 @@ extension ContentView {
}
var attachmentAnchor: PopoverAttachmentAnchor {
// Ideally .point(.bottom), but broken on Sonoma (FB12726503)
.rect(.bounds)
}

View File

@ -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()
}
}

View File

@ -4,13 +4,15 @@ import SecretKit
struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
@State var store: StoreType
@Binding var showing: Bool
@Environment(\.dismiss) private var dismiss
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,16 +96,24 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
}
}
}
if let errorText {
Section {
} footer: {
Text(verbatim: errorText)
.errorStyle()
}
}
}
HStack {
Toggle(.createSecretAdvancedLabel, isOn: $advanced)
.toggleStyle(.button)
Spacer()
Button(.createSecretCancelButton, role: .cancel) {
showing = false
dismiss()
}
Button(.createSecretCreateButton, action: save)
.primary()
.keyboardShortcut(.return)
.primaryButton()
.disabled(name.isEmpty)
}
.padding()
@ -117,20 +127,25 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: 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))
dismiss()
} catch {
errorText = error.localizedDescription
}
}
}
}
#Preview {
CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true))
CreateSecretView(store: Preview.StoreModifiable()) { _ in }
}

View File

@ -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)

View File

@ -30,21 +30,22 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
if let errorText {
Text(verbatim: errorText)
.foregroundStyle(.red)
.font(.callout)
} footer: {
if let errorText {
Text(verbatim: errorText)
.errorStyle()
}
}
}
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)
.primaryButton()
}
.padding()
}

View File

@ -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())
}
}

View File

@ -0,0 +1,350 @@
import SwiftUI
struct IntegrationsView: View {
@Environment(\.dismiss) private var dismiss
@State private var selectedInstruction: ConfigurationFileInstructions?
private let instructions = Instructions()
var body: some View {
NavigationSplitView {
List(selection: $selectedInstruction) {
ForEach(instructions.instructions) { group in
Section(group.name) {
ForEach(group.instructions) { instruction in
Text(instruction.tool)
.padding(.vertical, 8)
.tag(instruction)
}
}
}
}
} detail: {
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
.fauxToolbar {
Button(.setupDoneButton) {
dismiss()
}
.normalButton()
}
}
.onAppear {
selectedInstruction = instructions.gettingStarted
}
.frame(minHeight: 500)
}
}
extension View {
func fauxToolbar<Content: View>(content: () -> Content) -> some View {
modifier(FauxToolbarModifier(toolbarContent: content()))
}
}
struct FauxToolbarModifier<ToolbarContent: View>: 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)
}
}
}
}
struct IntegrationsDetailView: View {
@Binding private var selectedInstruction: ConfigurationFileInstructions?
private let instructions = Instructions()
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
_selectedInstruction = selectedInstruction
}
var body: some View {
if let selectedInstruction {
switch selectedInstruction.id {
case .gettingStarted:
Form {
Section(.integrationsGettingStartedTitle) {
Text(.integrationsGettingStartedTitleDescription)
}
Section {
Group {
Text(.integrationsGettingStartedSuggestionSsh)
.onTapGesture {
self.selectedInstruction = instructions.ssh
}
VStack(alignment: .leading, spacing: 5) {
Text(.integrationsGettingStartedSuggestionShell)
Text(.integrationsGettingStartedSuggestionShellDefault(shellName: instructions.defaultShell.tool))
.font(.caption2)
}
.onTapGesture {
self.selectedInstruction = instructions.defaultShell
}
Text(.integrationsGettingStartedSuggestionGit)
.onTapGesture {
self.selectedInstruction = instructions.git
}
}
.foregroundStyle(.link)
} header: {
Text(.integrationsGettingStartedWhatShouldIConfigureTitle)
}
footer: {
Text(.integrationsGettingStartedMultipleConfig)
}
}
.formStyle(.grouped)
case .tool:
Form {
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)) {
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(.integrationsWebLink)
.font(.headline)
Text(url.absoluteString)
.font(.caption2)
}
}
}
}
}
.formStyle(.grouped)
case .otherShell:
Form {
Section {
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!)
} header: {
Text(.integrationsCommunityShellListDescription)
.font(.body)
}
}
.formStyle(.grouped)
case .otherApp:
Form {
Section {
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!)
} header: {
Text(.integrationsCommunityAppsListDescription)
.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(.integrationsGettingStartedRowTitle, id: .gettingStarted)
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.",
)
}
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")!,
)
}
var zsh: ConfigurationFileInstructions {
ConfigurationFileInstructions(
tool: "zsh",
configPath: "~/.zshrc",
configText: "export SSH_AUTH_SOCK=\(socketPath)"
)
}
var instructions: [ConfigurationGroup] {
[
ConfigurationGroup(name: .integrationsGettingStartedSectionTitle, instructions: [
gettingStarted
]),
ConfigurationGroup(
name: .integrationsSystemSectionTitle,
instructions: [
ssh,
git,
]
),
ConfigurationGroup(name: .integrationsShellSectionTitle, instructions: [
zsh,
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(.integrationsOtherShellRowTitle, id: .otherShell),
]),
ConfigurationGroup(name: .integrationsOtherSectionTitle, instructions: [
ConfigurationFileInstructions(.integrationsAppsRowTitle, id: .otherApp),
]),
]
}
}
struct ConfigurationGroup: Identifiable {
let id = UUID()
var name: LocalizedStringResource
var instructions: [ConfigurationFileInstructions] = []
}
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?
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], note: note)]
self.website = website
}
init(tool: String, steps: [StepGroup], website: URL? = 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)
self.steps = []
}
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)
}

View File

@ -6,7 +6,7 @@ struct SecretDetailView<SecretType: Secret>: 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.agentHomeURL)
var body: some View {
ScrollView {
@ -37,12 +37,14 @@ struct SecretDetailView<SecretType: Secret>: View {
}
#if DEBUG
extension URL {
struct SecretDetailView_Previews: PreviewProvider {
static var previews: some View {
SecretDetailView(secret: Preview.Store(numberOfRandomSecrets: 1).secrets[0])
static var agentHomeURL: URL {
URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID))
}
}
#endif
#Preview {
SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret"))
}

View File

@ -2,227 +2,172 @@ import SwiftUI
struct SetupView: View {
@State var stepIndex = 0
@Binding var visible: Bool
@Environment(\.dismiss) private var dismiss
@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)
}
@State var showingIntegrations = false
@State var buttonWidth: CGFloat?
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..<numberOfSteps), id: \.self) { index in
ZStack {
if currentStep > index {
Circle()
.foregroundColor(.green)
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
Text(.setupStepCompleteSymbol)
.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<Content> : View where Content : View {
let title: LocalizedStringResource
let image: Image
let bodyText: LocalizedStringResource
let buttonTitle: LocalizedStringResource
let buttonAction: () -> Void
let content: Content
init(title: LocalizedStringResource, image: Image, bodyText: LocalizedStringResource, buttonTitle: LocalizedStringResource, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) {
self.title = title
self.image = image
self.bodyText = bodyText
self.buttonTitle = buttonTitle
self.buttonAction = buttonAction
self.content = content()
@State var installed = false
@State var updates = false
@State var integrations = false
var allDone: Bool {
installed && updates && integrations
}
var body: some View {
VStack {
Text(title)
.font(.title)
Spacer()
image
VStack(alignment: .leading, spacing: 0) {
StepView(
title: .setupAgentTitle,
description: .setupAgentDescription,
systemImage: "lock.laptopcomputer",
) {
setupButton(
.setupAgentInstallButton,
complete: installed,
width: buttonWidth
) {
installed = true
Task {
await LaunchAgentController().install()
}
}
}
Divider()
StepView(
title: .setupUpdatesTitle,
description: .setupUpdatesDescription,
systemImage: "network.badge.shield.half.filled",
) {
setupButton(
.setupUpdatesOkButton,
complete: updates,
width: buttonWidth
) {
updates = true
}
}
Divider()
StepView(
title: .setupIntegrationsTitle,
description: .setupIntegrationsDescription,
systemImage: "firewall",
) {
setupButton(
.setupIntegrationsButton,
complete: integrations,
width: buttonWidth
) {
showingIntegrations = true
}
}
}
.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(.setupDoneButton) {
setupComplete = true
dismiss()
}
.disabled(!allDone)
.primaryButton()
}
}
.interactiveDismissDisabled()
.padding()
.sheet(isPresented: $showingIntegrations, onDismiss: {
integrations = true
}, content: {
IntegrationsView()
})
}
}
struct setupButton: 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
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) {
if complete {
Text(.setupStepCompleteButton)
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)
.tint(complete ? .green : nil)
}
}
struct StepView<Content: View>: View {
let title: LocalizedStringResource
let icon: Image
let description: LocalizedStringResource
let 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(spacing: 20) {
icon
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 64)
Spacer()
Text(bodyText)
.multilineTextAlignment(.center)
Spacer()
content
Spacer()
Button(buttonTitle) {
buttonAction()
.frame(width: 24)
VStack(alignment: .leading, spacing: 6) {
Text(title)
.bold()
Text(description)
}
}.padding()
}
}
struct SecretAgentSetupView: View {
let buttonAction: () -> Void
var body: some View {
SetupStepView(title: .setupAgentTitle,
image: Image(nsImage: NSApplication.shared.applicationIconImage),
bodyText: .setupAgentDescription,
buttonTitle: .setupAgentInstallButton,
buttonAction: install) {
Text(.setupAgentActivityMonitorDescription)
.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: .setupSshTitle,
image: Image(systemName: "terminal"),
bodyText: .setupSshDescription,
buttonTitle: .setupSshAddedManuallyButton,
buttonAction: buttonAction) {
Link(.setupThirdPartyFaqLink, 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: .setupSshAddToConfigButton(configPath: selectedShellInstruction.shellConfigPath), image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text)
Button(.setupSshAddForMeButton) {
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: .setupUpdatesTitle,
image: Image(systemName: "dot.radiowaves.left.and.right"),
bodyText: .setupUpdatesDescription,
buttonTitle: .setupUpdatesOk,
buttonAction: buttonAction) {
Link(.setupUpdatesReadmore, destination: SetupView.Constants.updaterFAQURL)
Spacer()
actions
}
.padding(20)
}
}
@ -235,63 +180,6 @@ 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(setupComplete: .constant(false))
}
#if DEBUG
struct SetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
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

View File

@ -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
}
}