diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index 6e75bf7..505d7d9 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -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. It’s usually an email address." } } } @@ -2859,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." } } @@ -3008,6 +3132,424 @@ } } }, + "export SSH_AUTH_SOCK=%@" : { + "shouldTranslate" : false + }, + "Host *\n\tIdentityAgent %@" : { + "shouldTranslate" : false + }, + "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_configure_using_secret_empty_create" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You'll need to create a Secret before configuring this action." + } + } + } + }, + "integrations_configure_using_secret_header" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configure Using Secret" + } + } + } + }, + "integrations_configure_using_secret_no_secret" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Secret" + } + } + } + }, + "integrations_configure_using_secret_secret_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secret" + } + } + } + }, + "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_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" : { + "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_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" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shell" + } + } + } + }, + "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" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "System" + } + } + } + }, + "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" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "View on GitHub" + } + } + } + }, + "integrations_web_link" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "View Documentation on Web" + } + } + } + }, + "integrationsGitStepGitconfigSectionNote" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If any section (like [user]) already exists, just add the entries in the existing section." + } + } + } + }, "no_secure_storage_description" : { "extractionState" : "manual", "localizations" : { @@ -3395,6 +3937,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 +4445,9 @@ } } }, + "set -x SSH_AUTH_SOCK %@" : { + "shouldTranslate" : false + }, "setup_agent_activity_monitor_description" : { "extractionState" : "manual", "localizations" : { @@ -4176,150 +4732,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 +4847,6 @@ } } }, - "setup_ssh_description" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afegeix aquesta línia a la teua configuració del shell per que SSH es comunique amb Secretive quan vulga autenticar. Secretive pot fer aquest procediment automàticament, o pots copiar i pegar açò al teu fitxer de configuració." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Füge diese Zeile in deine Shell-Konfiguration ein, damit SSH zur Authentifizierung mit dem Secret Agent kommuniziert. Secretive kann dies automatisch tun, oder du kopierst diese Zeile in deine Konfigurationsdatei." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Add this line to your shell config telling SSH to talk to Secret Agent when it wants to authenticate. Secretive can either do this for you automatically, or you can copy and paste this into your config file." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ajoutez cette ligne à votre configuration shell pour indiquer à SSH de communiquer à Secret Agent quand il veut s'authentifier. Secretive peut le faire automatiquement pour vous, ou vous pouvez copier et coller cette ligne dans votre fichier de configuration." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aggiungi questa riga alla configurazione del Terminale per dire a SSH di parlare con Secret Agent quando vuole autenticarsi. Secretive può farlo automaticamente per te, oppure puoi copiare e incollare questa riga nel file di configurazione." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "以下の行をシェルの設定に追加してSSHが認証の際にSecretAgentを利用できるようにしてください。Secretiveが自動で追加するか、手動でコピーして設定に追加することもできます。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "SSH가 인증을 원할 때 Secret Agent와 통신하도록 지시하는 이 줄을 쉘 구성에 추가하세요. Secretive는 이 작업을 자동으로 수행하거나 사용자가 이를 복사하여 구성 파일에 붙여넣을 수 있습니다." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dodaj tą linijkę to pliku konfiguracyjnego SSH, aby nawiązać połączenie z Secret Agent kiedy potrzebna jest autoryzacja. Secretive może ustawić to automatycznie lub możesz to zrobić samodzielnie kopiując to do pliku konfiguracyjnego." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adicione esta linha nas configurações do seu shell para dizer ao SSH para falar com o Secret Agent quando ele necessitar de autenticação. Secretive pode fazer isto para você automaticamente ou você pode copiar e colar isso no seu arquivo de configuração." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Добавьте эту строчку к вашему конфигу shell, так SSH будет использовать SecretAgent в процессе аутентификации. Secretive может сделать это за Вас, либо Вы можете это скопировать сами." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "将以下文本添加到您的SSH 配置中以使用Secret Agent. Secretive 无法自动帮您完成该过程,或者您可以选择拷贝并粘贴该文本到您的配置文件中" - } - } - } - }, - "setup_ssh_title" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configura el teu agent SSH" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Konfiguriere deinen SSH Agent" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configure your SSH Agent" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurer votre Agent SSH" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configura il tuo Agente SSH" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "SSHエージェントを設定" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "SSH Agent 설정" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skonfiguruj twojego klienta SSH" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Configurar seu agente SSH" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Настройте Ваш SSH Agent" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "设置您的SSH 代理" - } - } - } - }, - "setup_step_complete_symbol" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "✓" - } - } - } - }, - "setup_third_party_faq_link" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Si tractes de configurar una aplicació de tercers, comprova el FAQ." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schaue dir die FAQs an, um eine Drittanbieter-App einzurichten." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "If you're trying to set up a third party app, check out the FAQ." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Si vous essayez de configurer une application tierce, consultez la FAQ." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se stai cercando di impostare un’app di terze parti, dai un'occhiata alla FAQ." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "その他のアプリから使う場合はよくある質問をご覧ください。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "타사 앱을 설정하려는 경우 FAQ를 확인하세요." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jeżeli próbujesz ustawić aplikacje stron trzecich, sprawdź FAQ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se você estiver tentando configurar um aplicativo de terceiros, verifique o FAQ." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Если Вы пытаетесь настроить сторонее приложение, ознакомьтесь с FAQ." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "如果您想设置第三方APP,请阅读 FAQ。" - } - } - } - }, "setup_updates_description" : { "extractionState" : "manual", "localizations" : { @@ -4750,7 +4918,7 @@ } } }, - "setup_updates_ok" : { + "setup_updates_ok_button" : { "extractionState" : "manual", "localizations" : { "ca" : { @@ -4963,6 +5131,17 @@ } } }, + "setupStepCompleteButton" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Done" + } + } + } + }, "signed_notification_description" : { "comment" : "When the user performs an action using a secret, they're shown a notification describing what happened. This is the description, showing which secret was used. The placeholder is the name of the secret.", "extractionState" : "manual", diff --git a/Sources/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/OpenSSH/OpenSSHCertificateHandler.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift index 23d64ce..ac4ced2 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHCertificateHandler.swift @@ -4,7 +4,7 @@ import OSLog /// Manages storage and lookup for OpenSSH certificates. public actor OpenSSHCertificateHandler: Sendable { - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) + private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory) private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler") private let writer = OpenSSHPublicKeyWriter() private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:] diff --git a/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift b/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift index 45bd222..49983d2 100644 --- a/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift +++ b/Sources/Packages/Sources/SecretKit/PublicKeyStandinFileController.swift @@ -5,12 +5,12 @@ import OSLog public final class PublicKeyFileStoreController: Sendable { private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController") - private let directory: String + private let directory: URL private let keyWriter = OpenSSHPublicKeyWriter() /// Initializes a PublicKeyFileStoreController. - public init(homeDirectory: String) { - directory = homeDirectory.appending("/PublicKeys") + public init(homeDirectory: URL) { + directory = homeDirectory.appending(component: "PublicKeys") } /// Writes out the keys specified to disk. @@ -20,7 +20,7 @@ public final class PublicKeyFileStoreController: Sendable { logger.log("Writing public keys to disk") if clear { let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) })) - let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory)) ?? [] + let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? [] let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" } let untracked = Set(fullPathContents) @@ -30,7 +30,7 @@ public final class PublicKeyFileStoreController: Sendable { try? FileManager.default.removeItem(at: URL(string: 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) @@ -45,14 +45,14 @@ public final class PublicKeyFileStoreController: Sendable { /// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to. public func publicKeyPath(for secret: SecretType) -> String { let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") - return directory.appending("/").appending("\(minimalHex).pub") + return directory.appending(component: "\(minimalHex).pub").path() } /// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory. public var hasAnyCertificates: Bool { do { return try FileManager.default - .contentsOfDirectory(atPath: directory) + .contentsOfDirectory(atPath: directory.path()) .filter { $0.hasSuffix("-cert.pub") } .isEmpty == false } catch { @@ -66,7 +66,7 @@ public final class PublicKeyFileStoreController: Sendable { /// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be. public func sshCertificatePath(for secret: SecretType) -> String { let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "") - return directory.appending("/").appending("\(minimalHex)-cert.pub") + return directory.appending(component: "\(minimalHex)-cert.pub").path() } } diff --git a/Sources/SecretAgent/AppDelegate.swift b/Sources/SecretAgent/AppDelegate.swift index 5800c75..877145e 100644 --- a/Sources/SecretAgent/AppDelegate.swift +++ b/Sources/SecretAgent/AppDelegate.swift @@ -21,7 +21,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { }() private let updater = Updater(checkOnLaunch: true) private let notifier = Notifier() - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory()) + private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory) private lazy var agent: Agent = { Agent(storeList: storeList, witness: notifier) }() diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index 459af4a..16d6f7e 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -26,6 +26,10 @@ 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 */; }; + 504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; }; + 504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; }; + 504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.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 */; }; @@ -36,7 +40,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 +52,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 */ @@ -107,6 +114,10 @@ 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 = ""; }; + 504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = ""; }; + 504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = ""; }; + 504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.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; }; @@ -120,7 +131,6 @@ 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = ""; }; 5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; }; 5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = ""; }; - 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = ""; }; 506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = ""; }; 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = ""; }; @@ -138,8 +148,12 @@ 50A3B79624026B7600D209EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 50A3B79824026B7600D209EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50A3B79924026B7600D209EA /* SecretAgent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgent.entitlements; sourceTree = ""; }; + 50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationsView.swift; sourceTree = ""; }; 50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = ""; }; 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = ""; }; + 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusView.swift; sourceTree = ""; }; + 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorStyle.swift; sourceTree = ""; }; + 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = ""; }; 50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = ""; }; 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -179,6 +193,55 @@ path = Helpers; sourceTree = ""; }; + 504788ED2E681EB200B4556F /* Styles */ = { + isa = PBXGroup; + children = ( + 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */, + 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */, + 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */, + ); + path = Styles; + sourceTree = ""; + }; + 504788EE2E681EC300B4556F /* Secrets */ = { + isa = PBXGroup; + children = ( + 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */, + 50B8550C24138C4F009958AC /* DeleteSecretView.swift */, + 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */, + 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */, + 506772C82425BB8500034DED /* NoStoresView.swift */, + 50C385A42407A76D00AF2719 /* SecretDetailView.swift */, + 50153E21250DECA300525160 /* SecretListItemView.swift */, + 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */, + ); + path = Secrets; + sourceTree = ""; + }; + 504788EF2E681ED700B4556F /* Configuration */ = { + isa = PBXGroup; + children = ( + 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */, + 50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */, + 504788F12E681F3A00B4556F /* Instructions.swift */, + 504788F32E681F6900B4556F /* ToolConfigurationView.swift */, + 5066A6C12516F303004B5A36 /* SetupView.swift */, + 504788F52E68206F00B4556F /* GettingStartedView.swift */, + ); + path = Configuration; + sourceTree = ""; + }; + 504788F02E681F0100B4556F /* Views */ = { + isa = PBXGroup; + children = ( + 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */, + 50617D8423FCE48E0099B055 /* ContentView.swift */, + 5066A6C72516FE6E004B5A36 /* CopyableView.swift */, + 50153E1F250AFCB200525160 /* UpdateView.swift */, + ); + path = Views; + sourceTree = ""; + }; 50617D7623FCE48D0099B055 = { isa = PBXGroup; children = ( @@ -241,20 +304,10 @@ 508A58B0241ED1C40069DC07 /* Views */ = { isa = PBXGroup; children = ( - 50617D8423FCE48E0099B055 /* ContentView.swift */, - 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */, - 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */, - 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */, - 50153E21250DECA300525160 /* SecretListItemView.swift */, - 50C385A42407A76D00AF2719 /* SecretDetailView.swift */, - 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */, - 50B8550C24138C4F009958AC /* DeleteSecretView.swift */, - 2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */, - 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */, - 506772C82425BB8500034DED /* NoStoresView.swift */, - 50153E1F250AFCB200525160 /* UpdateView.swift */, - 5066A6C12516F303004B5A36 /* SetupView.swift */, - 5066A6C72516FE6E004B5A36 /* CopyableView.swift */, + 504788EF2E681ED700B4556F /* Configuration */, + 504788EE2E681EC300B4556F /* Secrets */, + 504788ED2E681EB200B4556F /* Styles */, + 504788F02E681F0100B4556F /* Views */, ); path = Views; sourceTree = ""; @@ -262,11 +315,11 @@ 508A58B1241ED1EA0069DC07 /* Controllers */ = { isa = PBXGroup; children = ( + 504788EB2E680DC400B4556F /* URLs.swift */, 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */, 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, 50571E0424393D1500F76F6C /* LaunchAgentController.swift */, - 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */, ); path = Controllers; sourceTree = ""; @@ -433,26 +486,33 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 504788F22E681F3A00B4556F /* Instructions.swift in Sources */, + 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 */, + 504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */, 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */, 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 */, + 504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */, 506772C92425BB8500034DED /* NoStoresView.swift in Sources */, 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */, 508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */, @@ -647,10 +707,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 +747,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 +859,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 +893,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 +928,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 +964,17 @@ DEVELOPMENT_TEAM = Z72PRUAWF6; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; INFOPLIST_FILE = SecretAgent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/Sources/Secretive/App.swift b/Sources/Secretive/App.swift index 177beaf..93ecf03 100644 --- a/Sources/Secretive/App.swift +++ b/Sources/Secretive/App.swift @@ -37,6 +37,7 @@ struct Secretive: App { @Environment(\.agentStatusChecker) var agentStatusChecker @AppStorage("defaultsHasRunSetup") var hasRunSetup = false @State private var showingSetup = false + @State private var showingIntegrations = false @State private var showingCreation = false @SceneBuilder var body: some Scene { @@ -58,8 +59,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 @@ -71,11 +80,6 @@ struct Secretive: App { NSWorkspace.shared.open(Constants.helpURL) } } - CommandGroup(after: .help) { - Button(.appMenuSetupButton) { - showingSetup = true - } - } SidebarCommands() } } @@ -87,7 +91,7 @@ extension Secretive { private func reinstallAgent() { justUpdatedChecker.check() Task { - await LaunchAgentController().install() + _ = await LaunchAgentController().install() try? await Task.sleep(for: .seconds(1)) agentStatusChecker.check() if !agentStatusChecker.running { diff --git a/Sources/Secretive/Controllers/AgentStatusChecker.swift b/Sources/Secretive/Controllers/AgentStatusChecker.swift index 3c85f3f..b7327a6 100644 --- a/Sources/Secretive/Controllers/AgentStatusChecker.swift +++ b/Sources/Secretive/Controllers/AgentStatusChecker.swift @@ -6,12 +6,14 @@ import Observation @MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable { var running: Bool { get } var developmentBuild: Bool { get } + var process: NSRunningApplication? { get } func check() } @Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol { var running: Bool = false + var process: NSRunningApplication? = nil nonisolated init() { Task { @MainActor in @@ -20,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") + } + +} diff --git a/Sources/Secretive/Controllers/LaunchAgentController.swift b/Sources/Secretive/Controllers/LaunchAgentController.swift index a65f8b0..308c381 100644 --- a/Sources/Secretive/Controllers/LaunchAgentController.swift +++ b/Sources/Secretive/Controllers/LaunchAgentController.swift @@ -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() diff --git a/Sources/Secretive/Controllers/ShellConfigurationController.swift b/Sources/Secretive/Controllers/ShellConfigurationController.swift deleted file mode 100644 index 2ecb17e..0000000 --- a/Sources/Secretive/Controllers/ShellConfigurationController.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation -import Cocoa -import SecretKit - -struct ShellConfigurationController { - - let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.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 - } - -} 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/Helpers/BundleIDs.swift b/Sources/Secretive/Helpers/BundleIDs.swift index de4967d..bc84add 100644 --- a/Sources/Secretive/Helpers/BundleIDs.swift +++ b/Sources/Secretive/Helpers/BundleIDs.swift @@ -1,7 +1,11 @@ import Foundation - extension Bundle { - public var agentBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "Host", with: "SecretAgent"))!} - public var hostBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "SecretAgent", with: "Host"))!} + public static var agentBundleID: String { + Bundle.main.bundleIdentifier!.replacingOccurrences(of: "Host", with: "SecretAgent") + } + + public static var hostBundleID: String { + Bundle.main.bundleIdentifier!.replacingOccurrences(of: "SecretAgent", with: "Host") + } } diff --git a/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift b/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift index 51a5c09..e9799e9 100644 --- a/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift +++ b/Sources/Secretive/Preview Content/PreviewAgentStatusChecker.swift @@ -1,12 +1,15 @@ import Foundation +import AppKit class PreviewAgentStatusChecker: AgentStatusCheckerProtocol { let running: Bool + let process: NSRunningApplication? let developmentBuild = false - init(running: Bool = true) { + init(running: Bool = true, process: NSRunningApplication? = nil) { self.running = running + self.process = process } func check() { diff --git a/Sources/Secretive/Views/ActionButtonStyle.swift b/Sources/Secretive/Views/ActionButtonStyle.swift deleted file mode 100644 index 4d7455f..0000000 --- a/Sources/Secretive/Views/ActionButtonStyle.swift +++ /dev/null @@ -1,24 +0,0 @@ -import SwiftUI - -struct PrimaryButtonModifier: 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) - } else { - content.buttonStyle(.borderedProminent) - } - } - -} - -extension View { - - func primary() -> some View { - modifier(PrimaryButtonModifier()) - } - -} diff --git a/Sources/Secretive/Views/Configuration/ConfigurationItemView.swift b/Sources/Secretive/Views/Configuration/ConfigurationItemView.swift new file mode 100644 index 0000000..2c8520b --- /dev/null +++ b/Sources/Secretive/Views/Configuration/ConfigurationItemView.swift @@ -0,0 +1,59 @@ +import SwiftUI + +struct ConfigurationItemView: View { + + enum Action: Hashable { + case copy(String) + case revealInFinder(String) + } + + let title: LocalizedStringResource + let content: Content + let action: Action? + + init(title: LocalizedStringResource, value: String, action: Action? = nil) where Content == Text { + self.title = title + self.content = Text(value) + .font(.subheadline) + .foregroundStyle(.secondary) + self.action = action + } + + init(title: LocalizedStringResource, action: Action? = nil, content: () -> Content) { + self.title = title + self.content = content() + self.action = action + } + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text(title) + Spacer() + switch action { + case .copy(let string): + Button(.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 + } + } +} + diff --git a/Sources/Secretive/Views/Configuration/GettingStartedView.swift b/Sources/Secretive/Views/Configuration/GettingStartedView.swift new file mode 100644 index 0000000..67c7b42 --- /dev/null +++ b/Sources/Secretive/Views/Configuration/GettingStartedView.swift @@ -0,0 +1,49 @@ +import SwiftUI + +struct GettingStartedView: View { + + private let instructions = Instructions() + + @Binding var selectedInstruction: ConfigurationFileInstructions? + + init(selectedInstruction: Binding) { + _selectedInstruction = selectedInstruction + } + + var body: some View { + Form { + Section(.integrationsGettingStartedTitle) { + Text(.integrationsGettingStartedTitleDescription) + } + Section { + Group { + Text(.integrationsGettingStartedSuggestionSsh) + .onTapGesture { + self.selectedInstruction = instructions.ssh + } + VStack(alignment: .leading, spacing: 5) { + Text(.integrationsGettingStartedSuggestionShell) + Text(.integrationsGettingStartedSuggestionShellDefault(shellName: String(localized: 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) + } + +} diff --git a/Sources/Secretive/Views/Configuration/Instructions.swift b/Sources/Secretive/Views/Configuration/Instructions.swift new file mode 100644 index 0000000..bb92b86 --- /dev/null +++ b/Sources/Secretive/Views/Configuration/Instructions.swift @@ -0,0 +1,179 @@ +import Foundation + +struct Instructions { + + enum Constants { + static let publicKeyPathPlaceholder = "_PUBLIC_KEY_PATH_PLACEHOLDER_" + static let publicKeyPlaceholder = "_PUBLIC_KEY_PLACEHOLDER_" + } + + var defaultShell: ConfigurationFileInstructions { + zsh + } + + var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions(.integrationsGettingStartedRowTitle, id: .gettingStarted) + + var ssh: ConfigurationFileInstructions { + ConfigurationFileInstructions( + tool: LocalizedStringResource.integrationsToolNameSsh, + configPath: "~/.ssh/config", + configText: "Host *\n\tIdentityAgent \(URL.socketPath)", + website: URL(string: "https://man.openbsd.org/ssh_config.5")!, + note: .integrationsSshSpecificKeyNote, + ) + } + + var git: ConfigurationFileInstructions { + ConfigurationFileInstructions( + tool: .integrationsToolNameGitSigning, + steps: [ + .init(path: "~/.gitconfig", steps: [ + .integrationsGitStepGitconfigDescription(publicKeyPathPlaceholder: Constants.publicKeyPathPlaceholder) + ], + note: .integrationsGitStepGitconfigSectionNote + ), + .init( + path: "~/.gitallowedsigners", + steps: [ + LocalizedStringResource(stringLiteral: Constants.publicKeyPlaceholder) + ], + note: .integrationsGitStepGitallowedsignersDescription + ), + ], + website: URL(string: "https://git-scm.com/docs/git-config")!, + ) + } + + var zsh: ConfigurationFileInstructions { + ConfigurationFileInstructions( + tool: .integrationsToolNameZsh, + configPath: "~/.zshrc", + configText: "export SSH_AUTH_SOCK=\(URL.socketPath)" + ) + } + + var instructions: [ConfigurationGroup] { + [ + ConfigurationGroup(name: .integrationsGettingStartedSectionTitle, instructions: [ + gettingStarted + ]), + ConfigurationGroup( + name: .integrationsSystemSectionTitle, + instructions: [ + ssh, + git, + ] + ), + ConfigurationGroup(name: .integrationsShellSectionTitle, instructions: [ + zsh, + ConfigurationFileInstructions( + tool: .integrationsToolNameBash, + configPath: "~/.bashrc", + configText: "export SSH_AUTH_SOCK=\(URL.socketPath)" + ), + ConfigurationFileInstructions( + tool: .integrationsToolNameFish, + configPath: "~/.config/fish/config.fish", + configText: "set -x SSH_AUTH_SOCK \(URL.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: [LocalizedStringResource] + let note: LocalizedStringResource? + var id: String { path } + + 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: LocalizedStringResource + var steps: [StepGroup] + var requiresSecret: Bool + var website: URL? + + init( + tool: LocalizedStringResource, + configPath: String, + configText: LocalizedStringResource, + requiresSecret: Bool = false, + 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.requiresSecret = requiresSecret + self.website = website + } + + init( + tool: LocalizedStringResource, + steps: [StepGroup], + requiresSecret: Bool = false, + website: URL? = nil + ) { + self.id = .tool(String(localized: tool)) + self.tool = tool + self.steps = steps + self.requiresSecret = true + self.website = website + } + + init(_ name: LocalizedStringResource, id: ID) { + self.id = id + tool = name + steps = [] + requiresSecret = false + } + + func hash(into hasher: inout Hasher) { + id.hash(into: &hasher) + } + + 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" + } + } + } + +} diff --git a/Sources/Secretive/Views/Configuration/IntegrationsView.swift b/Sources/Secretive/Views/Configuration/IntegrationsView.swift new file mode 100644 index 0000000..de6b8a0 --- /dev/null +++ b/Sources/Secretive/Views/Configuration/IntegrationsView.swift @@ -0,0 +1,115 @@ +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: () -> Content) -> some View { + modifier(FauxToolbarModifier(toolbarContent: content())) + } + +} + +struct FauxToolbarModifier: ViewModifier { + + var toolbarContent: ToolbarContent + + func body(content: Content) -> some View { + VStack(alignment: .leading, spacing: 0) { + content + Divider() + HStack { + Spacer() + toolbarContent + .padding(.top, 8) + .padding(.trailing, 16) + .padding(.bottom, 16) + } + } + + } + +} + +struct IntegrationsDetailView: View { + + @Binding private var selectedInstruction: ConfigurationFileInstructions? + + init(selectedInstruction: Binding) { + _selectedInstruction = selectedInstruction + } + + var body: some View { + if let selectedInstruction { + switch selectedInstruction.id { + case .gettingStarted: + GettingStartedView(selectedInstruction: $selectedInstruction) + case .tool: + ToolConfigurationView(selectedInstruction: selectedInstruction) + 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) + } + } + + } + +} + +#Preview { + IntegrationsView() + .frame(height: 500) +} diff --git a/Sources/Secretive/Views/Configuration/SetupView.swift b/Sources/Secretive/Views/Configuration/SetupView.swift new file mode 100644 index 0000000..2c2d66e --- /dev/null +++ b/Sources/Secretive/Views/Configuration/SetupView.swift @@ -0,0 +1,187 @@ +import SwiftUI + +struct SetupView: View { + + @Environment(\.dismiss) private var dismiss + @Binding var setupComplete: Bool + + @State var showingIntegrations = false + @State var buttonWidth: CGFloat? + + @State var installed = false + @State var updates = false + @State var integrations = false + var allDone: Bool { + installed && updates && integrations + } + + var body: some View { + VStack { + 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: 600, 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: 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: 0) { + icon + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 24) + Spacer() + .frame(width: 20) + VStack(alignment: .leading, spacing: 4) { + Text(title) + .bold() + Text(description) + } + Spacer(minLength: 20) + actions + } + .padding(20) + } + +} + +extension SetupView { + + enum Constants { + static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")! + } + +} + +#Preview { + SetupView(setupComplete: .constant(false)) +} diff --git a/Sources/Secretive/Views/Configuration/ToolConfigurationView.swift b/Sources/Secretive/Views/Configuration/ToolConfigurationView.swift new file mode 100644 index 0000000..cd1bc69 --- /dev/null +++ b/Sources/Secretive/Views/Configuration/ToolConfigurationView.swift @@ -0,0 +1,110 @@ +import SwiftUI +import SecretKit + +struct ToolConfigurationView: View { + + private let instructions = Instructions() + let selectedInstruction: ConfigurationFileInstructions + + @Environment(\.secretStoreList) private var secretStoreList + + @State var creating = false + @State var selectedSecret: AnySecret? + + init(selectedInstruction: ConfigurationFileInstructions) { + self.selectedInstruction = selectedInstruction + } + + var body: some View { + Form { + if selectedInstruction.requiresSecret { + if secretStoreList.allSecrets.isEmpty { + Section { + Text(.integrationsConfigureUsingSecretEmptyCreate) + if let store = secretStoreList.modifiableStore { + HStack { + Spacer() + Button(.createSecretTitle) { + creating = true + } + .sheet(isPresented: $creating) { + CreateSecretView(store: store) { created in + selectedSecret = created + } + } + } + } + } + } else { + Section { + Picker(.integrationsConfigureUsingSecretSecretTitle, selection: $selectedSecret) { + if selectedSecret == nil { + Text(.integrationsConfigureUsingSecretNoSecret) + .tag(nil as (AnySecret?)) + } + ForEach(secretStoreList.allSecrets) { secret in + Text(secret.name) + .tag(secret) + } + } + } header: { + Text(.integrationsConfigureUsingSecretHeader) + } + .onAppear { + selectedSecret = secretStoreList.allSecrets.first + } + } + } + ForEach(selectedInstruction.steps) { stepGroup in + Section { + ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path)) + ForEach(stepGroup.steps, id: \.self.key) { step in + ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(String(localized: step))) { + HStack { + Text(placeholdersReplaced(text: String(localized: 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) + + } + + func placeholdersReplaced(text: String) -> String { + guard let selectedSecret else { return text } + let writer = OpenSSHPublicKeyWriter() + let fileController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL) + return text + .replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: writer.openSSHString(secret: selectedSecret)) + .replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: fileController.publicKeyPath(for: selectedSecret)) + } + +} diff --git a/Sources/Secretive/Views/CreateSecretView.swift b/Sources/Secretive/Views/Secrets/CreateSecretView.swift similarity index 84% rename from Sources/Secretive/Views/CreateSecretView.swift rename to Sources/Secretive/Views/Secrets/CreateSecretView.swift index b5f17b5..bd317ae 100644 --- a/Sources/Secretive/Views/CreateSecretView.swift +++ b/Sources/Secretive/Views/Secrets/CreateSecretView.swift @@ -4,13 +4,15 @@ import SecretKit struct CreateSecretView: View { @State var store: StoreType - @Binding var showing: Bool + @Environment(\.dismiss) private var dismiss + var createdSecret: (AnySecret?) -> Void @State private var name = "" @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: 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: 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 } } diff --git a/Sources/Secretive/Views/DeleteSecretView.swift b/Sources/Secretive/Views/Secrets/DeleteSecretView.swift similarity index 95% rename from Sources/Secretive/Views/DeleteSecretView.swift rename to Sources/Secretive/Views/Secrets/DeleteSecretView.swift index 2deee63..17f6610 100644 --- a/Sources/Secretive/Views/DeleteSecretView.swift +++ b/Sources/Secretive/Views/Secrets/DeleteSecretView.swift @@ -28,8 +28,7 @@ struct DeleteSecretConfirmationModifier: ViewModifier { TextField(secret.name, text: $confirmedSecretName) if let errorText { Text(verbatim: errorText) - .foregroundStyle(.red) - .font(.callout) + .errorStyle() } Button(.deleteConfirmationDeleteButton, action: delete) .disabled(confirmedSecretName != secret.name) diff --git a/Sources/Secretive/Views/EditSecretView.swift b/Sources/Secretive/Views/Secrets/EditSecretView.swift similarity index 90% rename from Sources/Secretive/Views/EditSecretView.swift rename to Sources/Secretive/Views/Secrets/EditSecretView.swift index cdc4114..80f5af0 100644 --- a/Sources/Secretive/Views/EditSecretView.swift +++ b/Sources/Secretive/Views/Secrets/EditSecretView.swift @@ -30,21 +30,22 @@ struct EditSecretView: View { .font(.subheadline) .foregroundStyle(.secondary) } - } - if let errorText { - Text(verbatim: errorText) - .foregroundStyle(.red) - .font(.callout) + } footer: { + if let errorText { + Text(verbatim: errorText) + .errorStyle() + } } } HStack { - 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() } diff --git a/Sources/Secretive/Views/EmptyStoreView.swift b/Sources/Secretive/Views/Secrets/EmptyStoreView.swift similarity index 100% rename from Sources/Secretive/Views/EmptyStoreView.swift rename to Sources/Secretive/Views/Secrets/EmptyStoreView.swift diff --git a/Sources/Secretive/Views/NoStoresView.swift b/Sources/Secretive/Views/Secrets/NoStoresView.swift similarity index 100% rename from Sources/Secretive/Views/NoStoresView.swift rename to Sources/Secretive/Views/Secrets/NoStoresView.swift diff --git a/Sources/Secretive/Views/SecretDetailView.swift b/Sources/Secretive/Views/Secrets/SecretDetailView.swift similarity index 80% rename from Sources/Secretive/Views/SecretDetailView.swift rename to Sources/Secretive/Views/Secrets/SecretDetailView.swift index 68a1e05..d9081ac 100644 --- a/Sources/Secretive/Views/SecretDetailView.swift +++ b/Sources/Secretive/Views/Secrets/SecretDetailView.swift @@ -6,8 +6,8 @@ struct SecretDetailView: View { let secret: SecretType private let keyWriter = OpenSSHPublicKeyWriter() - private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID)) - + private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL) + var body: some View { ScrollView { Form { @@ -37,12 +37,6 @@ struct SecretDetailView: View { } -#if DEBUG - -struct SecretDetailView_Previews: PreviewProvider { - static var previews: some View { - SecretDetailView(secret: Preview.Store(numberOfRandomSecrets: 1).secrets[0]) - } +#Preview { + SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret")) } - -#endif diff --git a/Sources/Secretive/Views/SecretListItemView.swift b/Sources/Secretive/Views/Secrets/SecretListItemView.swift similarity index 100% rename from Sources/Secretive/Views/SecretListItemView.swift rename to Sources/Secretive/Views/Secrets/SecretListItemView.swift diff --git a/Sources/Secretive/Views/StoreListView.swift b/Sources/Secretive/Views/Secrets/StoreListView.swift similarity index 100% rename from Sources/Secretive/Views/StoreListView.swift rename to Sources/Secretive/Views/Secrets/StoreListView.swift diff --git a/Sources/Secretive/Views/SetupView.swift b/Sources/Secretive/Views/SetupView.swift deleted file mode 100644 index e0a2560..0000000 --- a/Sources/Secretive/Views/SetupView.swift +++ /dev/null @@ -1,297 +0,0 @@ -import SwiftUI - -struct SetupView: View { - - @State var stepIndex = 0 - @Binding var visible: Bool - @Binding var setupComplete: Bool - - var body: some View { - GeometryReader { proxy in - VStack { - StepView(numberOfSteps: 3, currentStep: stepIndex, width: proxy.size.width) - GeometryReader { _ in - HStack(spacing: 0) { - SecretAgentSetupView(buttonAction: advance) - .frame(width: proxy.size.width) - SSHAgentSetupView(buttonAction: advance) - .frame(width: proxy.size.width) - UpdaterExplainerView { - visible = false - setupComplete = true - } - .frame(width: proxy.size.width) - } - .offset(x: -proxy.size.width * Double(stepIndex), y: 0) - } - } - } - .frame(minWidth: 500, idealWidth: 500, minHeight: 500, idealHeight: 500) - } - - - func advance() { - withAnimation(.spring()) { - stepIndex += 1 - } - } - -} - -struct StepView: View { - - let numberOfSteps: Int - let currentStep: Int - - // Ideally we'd have a geometry reader inside this view doing this for us, but that crashes on 11.0b7 - let width: Double - - var body: some View { - ZStack(alignment: .leading) { - Rectangle() - .foregroundColor(.blue) - .frame(height: 5) - Rectangle() - .foregroundColor(.green) - .frame(width: max(0, ((width - (Constants.padding * 2)) / Double(numberOfSteps - 1)) * Double(currentStep) - (Constants.circleWidth / 2)), height: 5) - HStack { - ForEach(Array(0.. index { - Circle() - .foregroundColor(.green) - .frame(width: Constants.circleWidth, height: Constants.circleWidth) - Text(.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 : 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() - } - - var body: some View { - VStack { - Text(title) - .font(.title) - Spacer() - image - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 64) - Spacer() - Text(bodyText) - .multilineTextAlignment(.center) - Spacer() - content - Spacer() - Button(buttonTitle) { - buttonAction() - } - }.padding() - } - -} - -struct SecretAgentSetupView: View { - - let buttonAction: () -> Void - - var body: some View { - SetupStepView(title: .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) - } - } - -} - -extension SetupView { - - enum Constants { - static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")! - } - -} - -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) - } - -} - -#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 diff --git a/Sources/Secretive/Views/Styles/ActionButtonStyle.swift b/Sources/Secretive/Views/Styles/ActionButtonStyle.swift new file mode 100644 index 0000000..74284a7 --- /dev/null +++ b/Sources/Secretive/Views/Styles/ActionButtonStyle.swift @@ -0,0 +1,94 @@ +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, isEnabled { + content.buttonStyle(.glassProminent) + } else { + content.buttonStyle(.borderedProminent) + } + } + +} + +extension 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()) + } + +} diff --git a/Sources/Secretive/Views/Styles/ErrorStyle.swift b/Sources/Secretive/Views/Styles/ErrorStyle.swift new file mode 100644 index 0000000..18917f1 --- /dev/null +++ b/Sources/Secretive/Views/Styles/ErrorStyle.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct ErrorStyleModifier: ViewModifier { + + func body(content: Content) -> some View { + content + .foregroundStyle(.red) + .font(.callout) + } + +} + +extension View { + + func errorStyle() -> some View { + modifier(ErrorStyleModifier()) + } + +} diff --git a/Sources/Secretive/Views/ToolbarButtonStyle.swift b/Sources/Secretive/Views/Styles/ToolbarButtonStyle.swift similarity index 100% rename from Sources/Secretive/Views/ToolbarButtonStyle.swift rename to Sources/Secretive/Views/Styles/ToolbarButtonStyle.swift diff --git a/Sources/Secretive/Views/Views/AgentStatusView.swift b/Sources/Secretive/Views/Views/AgentStatusView.swift new file mode 100644 index 0000000..28139fe --- /dev/null +++ b/Sources/Secretive/Views/Views/AgentStatusView.swift @@ -0,0 +1,153 @@ +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 + + 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: URL.socketPath, + action: .copy(URL.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)) +} diff --git a/Sources/Secretive/Views/ContentView.swift b/Sources/Secretive/Views/Views/ContentView.swift similarity index 78% rename from Sources/Secretive/Views/ContentView.swift rename to Sources/Secretive/Views/Views/ContentView.swift index dfc6dff..933013d 100644 --- a/Sources/Secretive/Views/ContentView.swift +++ b/Sources/Secretive/Views/Views/ContentView.swift @@ -36,7 +36,7 @@ struct ContentView: View { toolbarItem(newItemView, id: "new") } .sheet(isPresented: $runningSetup) { - SetupView(visible: $runningSetup, setupComplete: $hasRunSetup) + SetupView(setupComplete: $hasRunSetup) } } @@ -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) } diff --git a/Sources/Secretive/Views/CopyableView.swift b/Sources/Secretive/Views/Views/CopyableView.swift similarity index 88% rename from Sources/Secretive/Views/CopyableView.swift rename to Sources/Secretive/Views/Views/CopyableView.swift index d1be4be..23855f6 100644 --- a/Sources/Secretive/Views/CopyableView.swift +++ b/Sources/Secretive/Views/Views/CopyableView.swift @@ -76,10 +76,10 @@ struct CopyableView: View { switch interactionState { case .hovering: Image(systemName: "document.on.document") - .accessibilityLabel(String(localized: "copyable_click_to_copy_button")) + .accessibilityLabel(String(localized: .copyableClickToCopyButton)) case .clicking: Image(systemName: "checkmark.circle.fill") - .accessibilityLabel(String(localized: "copyable_copied")) + .accessibilityLabel(String(localized: .copyableCopied)) case .normal, .dragging: EmptyView() } @@ -168,9 +168,9 @@ fileprivate struct BackgroundViewModifier: ViewModifier { struct CopyableView_Previews: PreviewProvider { static var previews: some View { Group { - CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Hello world.") + CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.") .padding() - CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ") + CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ") .padding() } } diff --git a/Sources/Secretive/Views/UpdateView.swift b/Sources/Secretive/Views/Views/UpdateView.swift similarity index 100% rename from Sources/Secretive/Views/UpdateView.swift rename to Sources/Secretive/Views/Views/UpdateView.swift