Compare commits

..

18 Commits

Author SHA1 Message Date
Max Goedjen
df2b7881c4 WIP 2025-09-01 19:37:59 -07:00
Max Goedjen
74ddb9595b WIP 2025-09-01 19:31:16 -07:00
Max Goedjen
0980cdffcd WIP 2025-09-01 19:25:14 -07:00
Max Goedjen
90d55726bb WIP 2025-09-01 18:00:58 -07:00
Max Goedjen
a640d11b00 WIP 2025-09-01 17:50:40 -07:00
Max Goedjen
f3ce6b9d0f WIP 2025-09-01 17:43:33 -07:00
Max Goedjen
ea96dd88eb Cleanup 2025-09-01 16:27:15 -07:00
Max Goedjen
4d84621b3d WIP 2025-09-01 16:10:27 -07:00
Max Goedjen
2d05a7b0f3 WIP 2025-09-01 15:22:52 -07:00
Max Goedjen
c8d90ba455 WIP 2025-09-01 15:09:27 -07:00
Max Goedjen
9299bf343f WIP 2025-09-01 14:52:17 -07:00
Max Goedjen
fa658646d7 WIP 2025-08-31 13:24:37 -07:00
Max Goedjen
cd76bb95ec Tweaks. 2025-08-31 00:58:16 -07:00
Max Goedjen
b949d846c1 WIP 2025-08-30 18:56:52 -07:00
Max Goedjen
19760f1e02 Merge branch 'main' of github.com:maxgoedjen/secretive into newsetup 2025-08-30 15:40:52 -07:00
Max Goedjen
f60a44c599 WIP 2025-08-30 13:55:19 -07:00
Max Goedjen
260e63341d Merge branch 'main' into newsetup 2025-08-27 23:50:06 -07:00
Max Goedjen
cbf903deb7 WIP 2025-08-25 00:48:07 -07:00
37 changed files with 971 additions and 839 deletions

View File

@@ -57,7 +57,7 @@ let package = Package(
) )
var localization: Resource { var localization: Resource {
.process("../../Resources/Localizable.xcstrings") .process("../../Localizable.xcstrings")
} }
var swiftSettings: [PackageDescription.SwiftSetting] { var swiftSettings: [PackageDescription.SwiftSetting] {

View File

@@ -61,4 +61,4 @@ Because secrets in the Secure Enclave are not exportable, they are not able to b
## Security ## Security
Secretive's security policy is detailed in [SECURITY.md](SECURITY.md). To report security issues, please use [GitHub's private reporting feature.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) If you discover any vulnerabilities in this project, please notify [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with the subject containing "SECRETIVE SECURITY."

View File

@@ -24,4 +24,4 @@ The latest version on the [Releases page](https://github.com/maxgoedjen/secretiv
## Reporting a Vulnerability ## Reporting a Vulnerability
To report security issues, please use [GitHub's private reporting feature.](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) If you discover any vulnerabilities in this project, please notify max.goedjen@gmail.com with the subject containing "SECRETIVE SECURITY."

View File

@@ -2983,73 +2983,73 @@
"localizations" : { "localizations" : {
"ca" : { "ca" : {
"stringUnit" : { "stringUnit" : {
"state" : "needs_review", "state" : "translated",
"value" : "Secretive suporta claus EC256, EC384, RSA1024 i RSA2048." "value" : "Secretive suporta claus EC256, EC384, RSA1024 i RSA2048."
} }
}, },
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
"state" : "needs_review", "state" : "translated",
"value" : "Secretive unterstützt EC256, EC384, RSA1024 und RSA2048 Schlüssel." "value" : "Secretive unterstützt EC256, EC384, RSA1024 und RSA2048 Schlüssel."
} }
}, },
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Secretive supports EC256, EC384, and RSA2048 keys." "value" : "Secretive supports EC256, EC384, RSA1024, and RSA2048 keys."
} }
}, },
"fi" : { "fi" : {
"stringUnit" : { "stringUnit" : {
"state" : "needs_review", "state" : "translated",
"value" : "Secretive tukee EC256-, EC384-, RSA1024- ja RSA2048-avaimia." "value" : "Secretive tukee EC256-, EC384-, RSA1024- ja RSA2048-avaimia."
} }
}, },
"fr" : { "fr" : {
"stringUnit" : { "stringUnit" : {
"state" : "needs_review", "state" : "translated",
"value" : "Secretive prend en charge les clés EC256, EC384, RSA1024 et RSA2048." "value" : "Secretive prend en charge les clés EC256, EC384, RSA1024 et RSA2048."
} }
}, },
"it" : { "it" : {
"stringUnit" : { "stringUnit" : {
"state" : "needs_review", "state" : "translated",
"value" : "Secretive supporta la cifratura EC256, EC384, RSA1024 e RSA2048." "value" : "Secretive supporta la cifratura EC256, EC384, RSA1024 e RSA2048."
} }
}, },
"ja" : { "ja" : {
"stringUnit" : { "stringUnit" : {
"state" : "needs_review", "state" : "translated",
"value" : "SecretiveはEC256、EC384、RSA1024、またはRSA2048の鍵に対応しています。" "value" : "SecretiveはEC256、EC384、RSA1024、またはRSA2048の鍵に対応しています。"
} }
}, },
"ko" : { "ko" : {
"stringUnit" : { "stringUnit" : {
"state" : "needs_review", "state" : "translated",
"value" : "Secretive는 EC256, EC384, RSA1024 및 RSA2048 키를 지원합니다." "value" : "Secretive는 EC256, EC384, RSA1024 및 RSA2048 키를 지원합니다."
} }
}, },
"pl" : { "pl" : {
"stringUnit" : { "stringUnit" : {
"state" : "needs_review", "state" : "translated",
"value" : "Secretive wspiera klucze EC256, EC384, RSA1024 i RSA2048." "value" : "Secretive wspiera klucze EC256, EC384, RSA1024 i RSA2048."
} }
}, },
"pt-BR" : { "pt-BR" : {
"stringUnit" : { "stringUnit" : {
"state" : "needs_review", "state" : "translated",
"value" : "Secretive suporta chaves EC256, EC384, RSA1024 e RSA2048." "value" : "Secretive suporta chaves EC256, EC384, RSA1024 e RSA2048."
} }
}, },
"ru" : { "ru" : {
"stringUnit" : { "stringUnit" : {
"state" : "needs_review", "state" : "translated",
"value" : "Secretive поддерживает ключи EC256, EC384, RSA1024, и RSA2048." "value" : "Secretive поддерживает ключи EC256, EC384, RSA1024, и RSA2048."
} }
}, },
"zh-Hans" : { "zh-Hans" : {
"stringUnit" : { "stringUnit" : {
"state" : "needs_review", "state" : "translated",
"value" : "Secretive 支持 EC256, EC384, RSA1024, 和RSA2048." "value" : "Secretive 支持 EC256, EC384, RSA1024, 和RSA2048."
} }
} }
@@ -3132,12 +3132,6 @@
} }
} }
}, },
"export SSH_AUTH_SOCK=%@" : {
"shouldTranslate" : false
},
"Host *\n\tIdentityAgent %@" : {
"shouldTranslate" : false
},
"integrations_add_this_title" : { "integrations_add_this_title" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -3182,50 +3176,6 @@
} }
} }
}, },
"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" : { "integrations_getting_started_multiple_config" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -3336,40 +3286,6 @@
} }
} }
}, },
"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" : { "integrations_other_section_title" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -3403,28 +3319,6 @@
} }
} }
}, },
"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" : { "integrations_shell_section_title" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -3436,17 +3330,6 @@
} }
} }
}, },
"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" : { "integrations_system_section_title" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -3458,65 +3341,6 @@
} }
} }
}, },
"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" : { "integrations_view_other_github_link" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -3539,13 +3363,13 @@
} }
} }
}, },
"integrationsGitStepGitconfigSectionNote" : { "integrationsMenuBarTitle" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "If any section (like [user]) already exists, just add the entries in the existing section." "value" : "Integrations…"
} }
} }
} }
@@ -4445,8 +4269,15 @@
} }
} }
}, },
"set -x SSH_AUTH_SOCK %@" : { "Setup" : {
"shouldTranslate" : false "localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Setup"
}
}
}
}, },
"setup_agent_activity_monitor_description" : { "setup_agent_activity_monitor_description" : {
"extractionState" : "manual", "extractionState" : "manual",
@@ -4776,6 +4607,154 @@
} }
} }
}, },
"setup_ssh_add_for_me_button" : {
"extractionState" : "manual",
"localizations" : {
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "Afegeix-ho per mi"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Für Mich Einfügen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add it For Me"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ajoutez-le pour moi"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Aggiungila per me"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "自動で追加する"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "나를 위해 추가해주세요"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dodaj za mnie"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "Adicionar para mim"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Добавить для меня"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "为我添加"
}
}
}
},
"setup_ssh_add_to_config_button" : {
"extractionState" : "manual",
"localizations" : {
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "Afegeix a %1$(configPath)@"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "In %1$(configPath)@ einfügen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add to %1$(configPath)@"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add to %1$(configPath)@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ajouter à %1$(configPath)@"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Aggiungi a %1$(configPath)@"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$(configPath)@に追加"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$(configPath)@에 추가"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dodaj do %1$(configPath)@"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "Adicionar para %1$(configPath)@"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Добавить к %1$(configPath)@"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "添加到 %1$(configPath)@"
}
}
}
},
"setup_ssh_added_manually_button" : { "setup_ssh_added_manually_button" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -4847,6 +4826,290 @@
} }
} }
}, },
"setup_ssh_description" : {
"extractionState" : "manual",
"localizations" : {
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "Afegeix aquesta línia a la teua configuració del shell per que SSH es comunique amb Secretive quan vulga autenticar. Secretive pot fer aquest procediment automàticament, o pots copiar i pegar açò al teu fitxer de configuració."
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Füge diese Zeile in deine Shell-Konfiguration ein, damit SSH zur Authentifizierung mit dem Secret Agent kommuniziert. Secretive kann dies automatisch tun, oder du kopierst diese Zeile in deine Konfigurationsdatei."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add this line to your shell config telling SSH to talk to Secret Agent when it wants to authenticate. Secretive can either do this for you automatically, or you can copy and paste this into your config file."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ajoutez cette ligne à votre configuration shell pour indiquer à SSH de communiquer à Secret Agent quand il veut s'authentifier. Secretive peut le faire automatiquement pour vous, ou vous pouvez copier et coller cette ligne dans votre fichier de configuration."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Aggiungi questa riga alla configurazione del Terminale per dire a SSH di parlare con Secret Agent quando vuole autenticarsi. Secretive può farlo automaticamente per te, oppure puoi copiare e incollare questa riga nel file di configurazione."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "以下の行をシェルの設定に追加してSSHが認証の際にSecretAgentを利用できるようにしてください。Secretiveが自動で追加するか、手動でコピーして設定に追加することもできます。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "SSH가 인증을 원할 때 Secret Agent와 통신하도록 지시하는 이 줄을 쉘 구성에 추가하세요. Secretive는 이 작업을 자동으로 수행하거나 사용자가 이를 복사하여 구성 파일에 붙여넣을 수 있습니다."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dodaj tą linijkę to pliku konfiguracyjnego SSH, aby nawiązać połączenie z Secret Agent kiedy potrzebna jest autoryzacja. Secretive może ustawić to automatycznie lub możesz to zrobić samodzielnie kopiując to do pliku konfiguracyjnego."
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "Adicione esta linha nas configurações do seu shell para dizer ao SSH para falar com o Secret Agent quando ele necessitar de autenticação. Secretive pode fazer isto para você automaticamente ou você pode copiar e colar isso no seu arquivo de configuração."
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Добавьте эту строчку к вашему конфигу shell, так SSH будет использовать SecretAgent в процессе аутентификации. Secretive может сделать это за Вас, либо Вы можете это скопировать сами."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "将以下文本添加到您的SSH 配置中以使用Secret Agent. Secretive 无法自动帮您完成该过程,或者您可以选择拷贝并粘贴该文本到您的配置文件中"
}
}
}
},
"setup_ssh_title" : {
"extractionState" : "manual",
"localizations" : {
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "Configura el teu agent SSH"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Konfiguriere deinen SSH Agent"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Configure your SSH Agent"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Configurer votre Agent SSH"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Configura il tuo Agente SSH"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "SSHエージェントを設定"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "SSH Agent 설정"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Skonfiguruj twojego klienta SSH"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "Configurar seu agente SSH"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Настройте Ваш SSH Agent"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "设置您的SSH 代理"
}
}
}
},
"setup_step_complete_symbol" : {
"extractionState" : "manual",
"localizations" : {
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "✓"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "✓"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "✓"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "✓"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "✓"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "✓"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "✓"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "✓"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "✓"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "✓"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "✓"
}
}
}
},
"setup_third_party_faq_link" : {
"extractionState" : "manual",
"localizations" : {
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "Si tractes de configurar una aplicació de tercers, comprova el FAQ."
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Schaue dir die FAQs an, um eine Drittanbieter-App einzurichten."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "If you're trying to set up a third party app, check out the FAQ."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Si vous essayez de configurer une application tierce, consultez la FAQ."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Se stai cercando di impostare unapp di terze parti, dai un'occhiata alla FAQ."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "その他のアプリから使う場合はよくある質問をご覧ください。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "타사 앱을 설정하려는 경우 FAQ를 확인하세요."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Jeżeli próbujesz ustawić aplikacje stron trzecich, sprawdź FAQ."
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "Se você estiver tentando configurar um aplicativo de terceiros, verifique o FAQ."
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Если Вы пытаетесь настроить сторонее приложение, ознакомьтесь с FAQ."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "如果您想设置第三方APP请阅读 FAQ。"
}
}
}
},
"setup_updates_description" : { "setup_updates_description" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -5369,18 +5632,6 @@
} }
} }
}, },
"translationCredits" : {
"comment" : "Translated Into Language By\nFirst Translator, Second Translator, Third Translator",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : " "
}
}
}
},
"unnamed_secret" : { "unnamed_secret" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {

View File

@@ -82,7 +82,7 @@ let package = Package(
) )
var localization: Resource { var localization: Resource {
.process("../../Resources/Localizable.xcstrings") .process("../../Localizable.xcstrings")
} }
var swiftSettings: [PackageDescription.SwiftSetting] { var swiftSettings: [PackageDescription.SwiftSetting] {

View File

@@ -0,0 +1 @@

View File

@@ -89,8 +89,9 @@ extension Agent {
for secret in secrets { for secret in secrets {
let keyBlob = publicKeyWriter.data(secret: secret) let keyBlob = publicKeyWriter.data(secret: secret)
let curveData = publicKeyWriter.openSSHIdentifier(for: secret.keyType)
keyData.append(keyBlob.lengthAndData) keyData.append(keyBlob.lengthAndData)
keyData.append(publicKeyWriter.comment(secret: secret).lengthAndData) keyData.append(curveData.lengthAndData)
count += 1 count += 1
if let (certificateData, name) = try? await certificateHandler.keyBlobAndName(for: secret) { if let (certificateData, name) = try? await certificateHandler.keyBlobAndName(for: secret) {

View File

@@ -78,6 +78,7 @@ extension SocketController {
provenance = SigningRequestTracer().provenance(from: fileHandle) provenance = SigningRequestTracer().provenance(from: fileHandle)
(messages, messagesContinuation) = AsyncStream.makeStream() (messages, messagesContinuation) = AsyncStream.makeStream()
Task { [messagesContinuation, logger] in Task { [messagesContinuation, logger] in
await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor()
for await _ in NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle) { for await _ in NotificationCenter.default.notifications(named: .NSFileHandleDataAvailable, object: fileHandle) {
let data = fileHandle.availableData let data = fileHandle.availableData
guard !data.isEmpty else { guard !data.isEmpty else {
@@ -90,9 +91,6 @@ extension SocketController {
logger.debug("Socket controller yielded data.") logger.debug("Socket controller yielded data.")
} }
} }
Task {
await fileHandle.waitForDataInBackgroundAndNotifyOnMainActor()
}
} }
/// Writes new data to the socket. /// Writes new data to the socket.

View File

@@ -31,7 +31,18 @@ public struct OpenSSHPublicKeyWriter: Sendable {
/// Generates an OpenSSH string representation of the secret. /// Generates an OpenSSH string representation of the secret.
/// - Returns: OpenSSH string representation of the secret. /// - Returns: OpenSSH string representation of the secret.
public func openSSHString<SecretType: Secret>(secret: SecretType) -> String { public func openSSHString<SecretType: Secret>(secret: SecretType) -> String {
return [openSSHIdentifier(for: secret.keyType), data(secret: secret).base64EncodedString(), comment(secret: secret)] let resolvedComment: String
if let comment = secret.publicKeyAttribution {
resolvedComment = comment
} else {
let dashedKeyName = secret.name.replacingOccurrences(of: " ", with: "-")
let dashedHostName = ["secretive", Host.current().localizedName, "local"]
.compactMap { $0 }
.joined(separator: ".")
.replacingOccurrences(of: " ", with: "-")
resolvedComment = "\(dashedKeyName)@\(dashedHostName)"
}
return [openSSHIdentifier(for: secret.keyType), data(secret: secret).base64EncodedString(), resolvedComment]
.compactMap { $0 } .compactMap { $0 }
.joined(separator: " ") .joined(separator: " ")
} }
@@ -54,19 +65,6 @@ public struct OpenSSHPublicKeyWriter: Sendable {
.joined(separator: ":") .joined(separator: ":")
} }
public func comment<SecretType: Secret>(secret: SecretType) -> String {
if let comment = secret.publicKeyAttribution {
return comment
} else {
let dashedKeyName = secret.name.replacingOccurrences(of: " ", with: "-")
let dashedHostName = ["secretive", Host.current().localizedName, "local"]
.compactMap { $0 }
.joined(separator: ".")
.replacingOccurrences(of: " ", with: "-")
return "\(dashedKeyName)@\(dashedHostName)"
}
}
} }
extension OpenSSHPublicKeyWriter { extension OpenSSHPublicKeyWriter {

View File

@@ -26,8 +26,7 @@ public final class PublicKeyFileStoreController: Sendable {
let untracked = Set(fullPathContents) let untracked = Set(fullPathContents)
.subtracting(validPaths) .subtracting(validPaths)
for path in untracked { for path in untracked {
// string instead of fileURLWithPath since we're already using fileURL format. try? FileManager.default.removeItem(at: URL(fileURLWithPath: path))
try? FileManager.default.removeItem(at: URL(string: path)!)
} }
} }
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil) try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil)

View File

@@ -26,7 +26,7 @@ extension SecureEnclave {
for await note in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) { for await note in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) {
guard Constants.notificationToken != (note.object as? String) else { guard Constants.notificationToken != (note.object as? String) else {
// Don't reload if we're the ones triggering this by reloading. // Don't reload if we're the ones triggering this by reloading.
continue return
} }
reloadSecrets() reloadSecrets()
} }
@@ -112,7 +112,7 @@ extension SecureEnclave {
var accessError: SecurityError? var accessError: SecurityError?
let flags: SecAccessControlCreateFlags = switch attributes.authentication { let flags: SecAccessControlCreateFlags = switch attributes.authentication {
case .notRequired: case .notRequired:
[.privateKeyUsage] []
case .presenceRequired: case .presenceRequired:
[.userPresence, .privateKeyUsage] [.userPresence, .privateKeyUsage]
case .biometryCurrent: case .biometryCurrent:

View File

@@ -26,10 +26,6 @@
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; }; 50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; }; 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.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 */; }; 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; }; 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; }; 50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
@@ -110,14 +106,10 @@
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; }; 50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; }; 5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Resources/Localizable.xcstrings; sourceTree = SOURCE_ROOT; }; 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Localizable.xcstrings; sourceTree = SOURCE_ROOT; };
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; }; 50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; }; 50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
504788EB2E680DC400B4556F /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = "<group>"; };
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; };
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; }; 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; }; 50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; };
50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -193,55 +185,6 @@
path = Helpers; path = Helpers;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
504788ED2E681EB200B4556F /* Styles */ = {
isa = PBXGroup;
children = (
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
);
path = Styles;
sourceTree = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
504788F02E681F0100B4556F /* Views */ = {
isa = PBXGroup;
children = (
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
50617D8423FCE48E0099B055 /* ContentView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
50153E1F250AFCB200525160 /* UpdateView.swift */,
);
path = Views;
sourceTree = "<group>";
};
50617D7623FCE48D0099B055 = { 50617D7623FCE48D0099B055 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -304,10 +247,24 @@
508A58B0241ED1C40069DC07 /* Views */ = { 508A58B0241ED1C40069DC07 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
504788EF2E681ED700B4556F /* Configuration */, 50617D8423FCE48E0099B055 /* ContentView.swift */,
504788EE2E681EC300B4556F /* Secrets */, 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
504788ED2E681EB200B4556F /* Styles */, 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
504788F02E681F0100B4556F /* Views */, 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 */,
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -315,7 +272,6 @@
508A58B1241ED1EA0069DC07 /* Controllers */ = { 508A58B1241ED1EA0069DC07 /* Controllers */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
504788EB2E680DC400B4556F /* URLs.swift */,
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */, 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */, 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
@@ -486,15 +442,12 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
504788F22E681F3A00B4556F /* Instructions.swift in Sources */,
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */, 50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */,
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */, 2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */, 5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
504788EC2E680DC800B4556F /* URLs.swift in Sources */,
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */, 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */, 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */,
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */, 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */,
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
@@ -512,7 +465,6 @@
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */, 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */, 50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */,
50617D8323FCE48E0099B055 /* App.swift in Sources */, 50617D8323FCE48E0099B055 /* App.swift in Sources */,
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */,
506772C92425BB8500034DED /* NoStoresView.swift in Sources */, 506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */, 50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */, 508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,

View File

@@ -80,6 +80,11 @@ struct Secretive: App {
NSWorkspace.shared.open(Constants.helpURL) NSWorkspace.shared.open(Constants.helpURL)
} }
} }
CommandGroup(after: .help) {
Button("Setup") {
showingSetup = true
}
}
SidebarCommands() SidebarCommands()
} }
} }

View File

@@ -1,12 +0,0 @@
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()
}
}

View File

@@ -15,6 +15,7 @@ struct AgentStatusView: View {
struct AgentRunningView: View { struct AgentRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol @Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
var body: some View { var body: some View {
Form { Form {
@@ -27,8 +28,8 @@ struct AgentRunningView: View {
) )
ConfigurationItemView( ConfigurationItemView(
title: .agentDetailsSocketPathTitle, title: .agentDetailsSocketPathTitle,
value: URL.socketPath, value: socketPath,
action: .copy(URL.socketPath), action: .copy(socketPath),
) )
ConfigurationItemView( ConfigurationItemView(
title: .agentDetailsVersionTitle, title: .agentDetailsVersionTitle,

View File

@@ -1,49 +0,0 @@
import SwiftUI
struct GettingStartedView: View {
private let instructions = Instructions()
@Binding var selectedInstruction: ConfigurationFileInstructions?
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
_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)
}
}

View File

@@ -1,179 +0,0 @@
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"
}
}
}
}

View File

@@ -1,115 +0,0 @@
import SwiftUI
struct IntegrationsView: View {
@Environment(\.dismiss) private var dismiss
@State private var selectedInstruction: ConfigurationFileInstructions?
private let instructions = Instructions()
var body: some View {
NavigationSplitView {
List(selection: $selectedInstruction) {
ForEach(instructions.instructions) { group in
Section(group.name) {
ForEach(group.instructions) { instruction in
Text(instruction.tool)
.padding(.vertical, 8)
.tag(instruction)
}
}
}
}
} detail: {
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
.fauxToolbar {
Button(.setupDoneButton) {
dismiss()
}
.normalButton()
}
}
.onAppear {
selectedInstruction = instructions.gettingStarted
}
.frame(minHeight: 500)
}
}
extension View {
func fauxToolbar<Content: View>(content: () -> Content) -> some View {
modifier(FauxToolbarModifier(toolbarContent: content()))
}
}
struct FauxToolbarModifier<ToolbarContent: View>: ViewModifier {
var toolbarContent: ToolbarContent
func body(content: Content) -> some View {
VStack(alignment: .leading, 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<ConfigurationFileInstructions?>) {
_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)
}

View File

@@ -1,110 +0,0 @@
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))
}
}

View File

@@ -32,7 +32,7 @@ struct ConfigurationItemView<Content: View>: View {
Spacer() Spacer()
switch action { switch action {
case .copy(let string): case .copy(let string):
Button(.copyableClickToCopyButton, systemImage: "document.on.document") { Button(.copyButton, systemImage: "document.on.document") {
NSPasteboard.general.declareTypes([.string], owner: nil) NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(string, forType: .string) NSPasteboard.general.setString(string, forType: .string)
} }

View File

@@ -76,10 +76,10 @@ struct CopyableView: View {
switch interactionState { switch interactionState {
case .hovering: case .hovering:
Image(systemName: "document.on.document") Image(systemName: "document.on.document")
.accessibilityLabel(String(localized: .copyableClickToCopyButton)) .accessibilityLabel(String(localized: "copyable_click_to_copy_button"))
case .clicking: case .clicking:
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.accessibilityLabel(String(localized: .copyableCopied)) .accessibilityLabel(String(localized: "copyable_copied"))
case .normal, .dragging: case .normal, .dragging:
EmptyView() EmptyView()
} }
@@ -168,9 +168,9 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
struct CopyableView_Previews: PreviewProvider { struct CopyableView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Group { Group {
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.") CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Hello world.")
.padding() .padding()
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. ") 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. ")
.padding() .padding()
} }
} }

View File

@@ -54,7 +54,9 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
func rename() { func rename() {
var attributes = secret.attributes var attributes = secret.attributes
attributes.publicKeyAttribution = publicKeyAttribution.isEmpty ? nil : publicKeyAttribution if !publicKeyAttribution.isEmpty {
attributes.publicKeyAttribution = publicKeyAttribution
}
Task { Task {
do { do {
try await store.update(secret: secret, name: name, attributes: attributes) try await store.update(secret: secret, name: name, attributes: attributes)

View File

@@ -0,0 +1,350 @@
import SwiftUI
struct IntegrationsView: View {
@Environment(\.dismiss) private var dismiss
@State private var selectedInstruction: ConfigurationFileInstructions?
private let instructions = Instructions()
var body: some View {
NavigationSplitView {
List(selection: $selectedInstruction) {
ForEach(instructions.instructions) { group in
Section(group.name) {
ForEach(group.instructions) { instruction in
Text(instruction.tool)
.padding(.vertical, 8)
.tag(instruction)
}
}
}
}
} detail: {
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
.fauxToolbar {
Button(.setupDoneButton) {
dismiss()
}
.normalButton()
}
}
.onAppear {
selectedInstruction = instructions.gettingStarted
}
.frame(minHeight: 500)
}
}
extension View {
func fauxToolbar<Content: View>(content: () -> Content) -> some View {
modifier(FauxToolbarModifier(toolbarContent: content()))
}
}
struct FauxToolbarModifier<ToolbarContent: View>: ViewModifier {
var toolbarContent: ToolbarContent
func body(content: Content) -> some View {
VStack(alignment: .leading) {
content
Divider()
HStack {
Spacer()
toolbarContent
.padding(.top, 8)
.padding(.trailing, 16)
.padding(.bottom, 16)
}
}
}
}
struct IntegrationsDetailView: View {
@Binding private var selectedInstruction: ConfigurationFileInstructions?
private let instructions = Instructions()
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
_selectedInstruction = selectedInstruction
}
var body: some View {
if let selectedInstruction {
switch selectedInstruction.id {
case .gettingStarted:
Form {
Section(.integrationsGettingStartedTitle) {
Text(.integrationsGettingStartedTitleDescription)
}
Section {
Group {
Text(.integrationsGettingStartedSuggestionSsh)
.onTapGesture {
self.selectedInstruction = instructions.ssh
}
VStack(alignment: .leading, spacing: 5) {
Text(.integrationsGettingStartedSuggestionShell)
Text(.integrationsGettingStartedSuggestionShellDefault(shellName: instructions.defaultShell.tool))
.font(.caption2)
}
.onTapGesture {
self.selectedInstruction = instructions.defaultShell
}
Text(.integrationsGettingStartedSuggestionGit)
.onTapGesture {
self.selectedInstruction = instructions.git
}
}
.foregroundStyle(.link)
} header: {
Text(.integrationsGettingStartedWhatShouldIConfigureTitle)
}
footer: {
Text(.integrationsGettingStartedMultipleConfig)
}
}
.formStyle(.grouped)
case .tool:
Form {
ForEach(selectedInstruction.steps) { stepGroup in
Section {
ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path))
ForEach(stepGroup.steps, id: \.self) { step in
ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(step)) {
HStack {
Text(step)
.padding(8)
.font(.system(.subheadline, design: .monospaced))
Spacer()
}
.frame(maxWidth: .infinity)
.background {
RoundedRectangle(cornerRadius: 6)
.fill(.black.opacity(0.05))
.stroke(.separator, lineWidth: 1)
}
}
}
} footer: {
if let note = stepGroup.note {
Text(note)
.font(.caption)
}
}
}
if let url = selectedInstruction.website {
Section {
Link(destination: url) {
VStack(alignment: .leading, spacing: 5) {
Text(.integrationsWebLink)
.font(.headline)
Text(url.absoluteString)
.font(.caption2)
}
}
}
}
}
.formStyle(.grouped)
case .otherShell:
Form {
Section {
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!)
} header: {
Text(.integrationsCommunityShellListDescription)
.font(.body)
}
}
.formStyle(.grouped)
case .otherApp:
Form {
Section {
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!)
} header: {
Text(.integrationsCommunityAppsListDescription)
.font(.body)
}
}
.formStyle(.grouped)
}
}
}
}
private struct Instructions {
private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
var defaultShell: ConfigurationFileInstructions {
zsh
}
var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions(.integrationsGettingStartedRowTitle, id: .gettingStarted)
var ssh: ConfigurationFileInstructions {
ConfigurationFileInstructions(
tool: "SSH",
configPath: "~/.ssh/config",
configText: "Host *\n\tIdentityAgent \(socketPath)",
website: URL(string: "https://man.openbsd.org/ssh_config.5")!,
note: "You can tell SSH to use a specific key for a given host. See the web documentation for more details.",
)
}
var git: ConfigurationFileInstructions {
ConfigurationFileInstructions(
tool: "Git Signing",
steps: [
.init(path: "~/.gitconfig", steps: [
"""
[user]
signingkey = YOUR_PUBLIC_KEY_PATH
[commit]
gpgsign = true
[gpg]
format = ssh
[gpg "ssh"]
allowedSignersFile = ~/.gitallowedsigners
"""
],
note: "If any section (like [user]) already exists, just add the entries in the existing section."
),
.init(
path: "~/.gitallowedsigners",
steps: [
"YOUR_PUBLIC_KEY"
],
note: "~/.gitallowedsigners probably does not exist. You'll need to create it."
),
],
website: URL(string: "https://git-scm.com/docs/git-config")!,
)
}
var zsh: ConfigurationFileInstructions {
ConfigurationFileInstructions(
tool: "zsh",
configPath: "~/.zshrc",
configText: "export SSH_AUTH_SOCK=\(socketPath)"
)
}
var instructions: [ConfigurationGroup] {
[
ConfigurationGroup(name: .integrationsGettingStartedSectionTitle, instructions: [
gettingStarted
]),
ConfigurationGroup(
name: .integrationsSystemSectionTitle,
instructions: [
ssh,
git,
]
),
ConfigurationGroup(name: .integrationsShellSectionTitle, instructions: [
zsh,
ConfigurationFileInstructions(
tool: "bash",
configPath: "~/.bashrc",
configText: "export SSH_AUTH_SOCK=\(socketPath)"
),
ConfigurationFileInstructions(
tool: "fish",
configPath: "~/.config/fish/config.fish",
configText: "set -x SSH_AUTH_SOCK \(socketPath)"
),
ConfigurationFileInstructions(.integrationsOtherShellRowTitle, id: .otherShell),
]),
ConfigurationGroup(name: .integrationsOtherSectionTitle, instructions: [
ConfigurationFileInstructions(.integrationsAppsRowTitle, id: .otherApp),
]),
]
}
}
struct ConfigurationGroup: Identifiable {
let id = UUID()
var name: LocalizedStringResource
var instructions: [ConfigurationFileInstructions] = []
}
struct ConfigurationFileInstructions: Hashable, Identifiable {
struct StepGroup: Hashable, Identifiable {
let path: String
let steps: [String]
let note: String?
var id: String { path }
init(path: String, steps: [String], note: String? = nil) {
self.path = path
self.steps = steps
self.note = note
}
}
var id: ID
var tool: String
var steps: [StepGroup]
var website: URL?
init(tool: String, configPath: String, configText: String, website: URL? = nil, note: String? = nil) {
self.id = .tool(tool)
self.tool = tool
self.steps = [StepGroup(path: configPath, steps: [configText], note: note)]
self.website = website
}
init(tool: String, steps: [StepGroup], website: URL? = nil) {
self.id = .tool(tool)
self.tool = tool
self.steps = steps
self.website = website
}
init(_ name: LocalizedStringResource, id: ID) {
self.id = id
tool = String(localized: name)
self.steps = []
}
enum ID: Identifiable, Hashable {
case gettingStarted
case tool(String)
case otherShell
case otherApp
var id: String {
switch self {
case .gettingStarted:
"getting_started"
case .tool(let name):
name
case .otherShell:
"other_shell"
case .otherApp:
"other_app"
}
}
}
}
#Preview {
IntegrationsView()
.frame(height: 500)
}

View File

@@ -37,6 +37,14 @@ struct SecretDetailView<SecretType: Secret>: View {
} }
extension URL {
static var agentHomeURL: URL {
URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID))
}
}
#Preview { #Preview {
SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret")) SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret"))
} }

View File

@@ -67,7 +67,7 @@ struct SetupView: View {
buttonWidth = width buttonWidth = width
} }
.background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10)) .background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
.frame(minWidth: 600, maxWidth: .infinity) .frame(minWidth: 700, maxWidth: .infinity)
HStack { HStack {
Spacer() Spacer()
Button(.setupDoneButton) { Button(.setupDoneButton) {
@@ -154,19 +154,17 @@ struct StepView<Content: View>: View {
} }
var body: some View { var body: some View {
HStack(spacing: 0) { HStack(spacing: 20) {
icon icon
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 24) .frame(width: 24)
Spacer() VStack(alignment: .leading, spacing: 6) {
.frame(width: 20)
VStack(alignment: .leading, spacing: 4) {
Text(title) Text(title)
.bold() .bold()
Text(description) Text(description)
} }
Spacer(minLength: 20) Spacer()
actions actions
} }
.padding(20) .padding(20)

View File

@@ -0,0 +1,96 @@
import SwiftUI
import Brief
struct UpdateDetailView: View {
@Environment(\.updater) var updater: any UpdaterProtocol
let update: Release
var body: some View {
VStack {
Text(.updateVersionName(updateName: update.name)).font(.title)
GroupBox(label: Text(.updateReleaseNotesTitle)) {
ScrollView {
Text(attributedBody)
}
}
HStack {
if !update.critical {
Button(.updateIgnoreButton) {
Task {
await updater.ignore(release: update)
}
}
Spacer()
}
Button(.updateUpdateButton) {
NSWorkspace.shared.open(update.html_url)
}
.keyboardShortcut(.defaultAction)
}
}
.padding()
.frame(maxWidth: 500)
}
var attributedBody: AttributedString {
do {
var text = try AttributedString(
markdown: update.body,
options: .init(
allowsExtendedAttributes: true,
interpretedSyntax: .full,
),
baseURL: URL(string: "https://github.com/maxgoedjen/secretive")!
)
.transformingAttributes(AttributeScopes.FoundationAttributes.PresentationIntentAttribute.self) { key in
let font: Font? = switch key.value?.components.first?.kind {
case .header(level: 1):
Font.title
case .header(level: 2):
Font.title2
case .header(level: 3):
Font.title3
default:
nil
}
if let font {
key.replace(with: AttributeScopes.SwiftUIAttributes.FontAttribute.self, value: font)
}
}
let lineBreak = AttributedString("\n\n")
for run in text.runs.reversed() {
text.insert(lineBreak, at: run.range.lowerBound)
}
return text
} catch {
var text = AttributedString()
for line in update.body.split(whereSeparator: \.isNewline) {
let attributed: AttributedString
let split = line.split(separator: " ")
let unprefixed = split.dropFirst().joined(separator: " ")
if let prefix = split.first {
var container = AttributeContainer()
switch prefix {
case "#":
container.font = .title
case "##":
container.font = .title2
case "###":
container.font = .title3
default:
continue
}
attributed = AttributedString(unprefixed, attributes: container)
} else {
attributed = AttributedString(line + "\n\n")
}
text = text + attributed
}
return text
}
}
}

View File

@@ -1,63 +0,0 @@
import SwiftUI
import Brief
struct UpdateDetailView: View {
@Environment(\.updater) var updater: any UpdaterProtocol
let update: Release
var body: some View {
VStack {
Text(.updateVersionName(updateName: update.name)).font(.title)
GroupBox(label: Text(.updateReleaseNotesTitle)) {
ScrollView {
attributedBody
}
}
HStack {
if !update.critical {
Button(.updateIgnoreButton) {
Task {
await updater.ignore(release: update)
}
}
Spacer()
}
Button(.updateUpdateButton) {
NSWorkspace.shared.open(update.html_url)
}
.keyboardShortcut(.defaultAction)
}
}
.padding()
.frame(maxWidth: 500)
}
var attributedBody: Text {
var text = Text(verbatim: "")
for line in update.body.split(whereSeparator: \.isNewline) {
let attributed: Text
let split = line.split(separator: " ")
let unprefixed = split.dropFirst().joined(separator: " ")
if let prefix = split.first {
switch prefix {
case "#":
attributed = Text(unprefixed).font(.title) + Text(verbatim: "\n")
case "##":
attributed = Text(unprefixed).font(.title2) + Text(verbatim: "\n")
case "###":
attributed = Text(unprefixed).font(.title3) + Text(verbatim: "\n")
default:
attributed = Text(line) + Text(verbatim: "\n\n")
}
} else {
attributed = Text(line) + Text(verbatim: "\n\n")
}
text = text + attributed
}
return text
}
}