mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-04-10 03:07:22 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4615c7d3a | ||
|
|
a4e1ab9eb6 | ||
|
|
147f4d9908 | ||
|
|
ddcb2a36ec |
@@ -57,7 +57,7 @@ let package = Package(
|
|||||||
)
|
)
|
||||||
|
|
||||||
var localization: Resource {
|
var localization: Resource {
|
||||||
.process("../../Localizable.xcstrings")
|
.process("../../Resources/Localizable.xcstrings")
|
||||||
}
|
}
|
||||||
|
|
||||||
var swiftSettings: [PackageDescription.SwiftSetting] {
|
var swiftSettings: [PackageDescription.SwiftSetting] {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ let package = Package(
|
|||||||
)
|
)
|
||||||
|
|
||||||
var localization: Resource {
|
var localization: Resource {
|
||||||
.process("../../Localizable.xcstrings")
|
.process("../../Resources/Localizable.xcstrings")
|
||||||
}
|
}
|
||||||
|
|
||||||
var swiftSettings: [PackageDescription.SwiftSetting] {
|
var swiftSettings: [PackageDescription.SwiftSetting] {
|
||||||
|
|||||||
@@ -2983,73 +2983,73 @@
|
|||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ca" : {
|
"ca" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive suporta claus EC256, EC384, RSA1024 i RSA2048."
|
"value" : "Secretive suporta claus EC256, EC384, RSA1024 i RSA2048."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"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, RSA1024, and RSA2048 keys."
|
"value" : "Secretive supports EC256, EC384, and RSA2048 keys."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fi" : {
|
"fi" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive tukee EC256-, EC384-, RSA1024- ja RSA2048-avaimia."
|
"value" : "Secretive tukee EC256-, EC384-, RSA1024- ja RSA2048-avaimia."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"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" : "translated",
|
"state" : "needs_review",
|
||||||
"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" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "SecretiveはEC256、EC384、RSA1024、またはRSA2048の鍵に対応しています。"
|
"value" : "SecretiveはEC256、EC384、RSA1024、またはRSA2048の鍵に対応しています。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ko" : {
|
"ko" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive는 EC256, EC384, RSA1024 및 RSA2048 키를 지원합니다."
|
"value" : "Secretive는 EC256, EC384, RSA1024 및 RSA2048 키를 지원합니다."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pl" : {
|
"pl" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"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" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive suporta chaves EC256, EC384, RSA1024 e RSA2048."
|
"value" : "Secretive suporta chaves EC256, EC384, RSA1024 e RSA2048."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive поддерживает ключи EC256, EC384, RSA1024, и RSA2048."
|
"value" : "Secretive поддерживает ключи EC256, EC384, RSA1024, и RSA2048."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive 支持 EC256, EC384, RSA1024, 和RSA2048."
|
"value" : "Secretive 支持 EC256, EC384, RSA1024, 和RSA2048."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3132,6 +3132,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"export SSH_AUTH_SOCK=%@" : {
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
|
"Host *\n\tIdentityAgent %@" : {
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
"integrations_add_this_title" : {
|
"integrations_add_this_title" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3176,6 +3182,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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" : {
|
||||||
@@ -3286,6 +3336,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"integrations_git_step_gitallowedsigners_description" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "~/.gitallowedsigners probably does not exist. You'll need to create it."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"integrations_git_step_gitconfig_description" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "[user]\n signingkey = %1$(publicKeyPathPlaceholder)@\n[commit]\n gpgsign = true\n[gpg]\n format = ssh\n[gpg \"ssh\"]\n allowedSignersFile = ~/.gitallowedsigners"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
|
"integrations_menu_bar_title" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Integrations…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"integrations_other_section_title" : {
|
"integrations_other_section_title" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3319,6 +3403,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"integrations_public_key_path_placeholder" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "YOUR_PUBLIC_KEY_PATH"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"integrations_public_key_placeholder" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "YOUR_PUBLIC_KEY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"integrations_shell_section_title" : {
|
"integrations_shell_section_title" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3330,6 +3436,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"integrations_ssh_specific_key_note" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "You can tell SSH to use a specific key for a given host. See the web documentation for more details."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"integrations_system_section_title" : {
|
"integrations_system_section_title" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3341,6 +3458,65 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"integrations_tool_name_bash" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "bash"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
|
"integrations_tool_name_fish" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "fish"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
|
"integrations_tool_name_git_signing" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Git Signing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"integrations_tool_name_ssh" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "SSH"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
|
"integrations_tool_name_zsh" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "zsh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
"integrations_view_other_github_link" : {
|
"integrations_view_other_github_link" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3363,13 +3539,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"integrationsMenuBarTitle" : {
|
"integrationsGitStepGitconfigSectionNote" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Integrations…"
|
"value" : "If any section (like [user]) already exists, just add the entries in the existing section."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4269,16 +4445,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Setup" : {
|
"set -x SSH_AUTH_SOCK %@" : {
|
||||||
"extractionState" : "stale",
|
"shouldTranslate" : false
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Setup"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"setup_agent_activity_monitor_description" : {
|
"setup_agent_activity_monitor_description" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
@@ -5201,8 +5369,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Test" : {
|
"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",
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -26,7 +26,8 @@ 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 {
|
||||||
try? FileManager.default.removeItem(at: URL(fileURLWithPath: path))
|
// string instead of fileURLWithPath since we're already using fileURL format.
|
||||||
|
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)
|
||||||
|
|||||||
@@ -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.
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
reloadSecrets()
|
reloadSecrets()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,85 +6,65 @@ import SmartCardSecretKit
|
|||||||
import SecretAgentKit
|
import SecretAgentKit
|
||||||
import Brief
|
import Brief
|
||||||
import Observation
|
import Observation
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
|
|
||||||
extension EnvironmentValues {
|
|
||||||
private static let _agentStatusChecker = AgentStatusChecker()
|
|
||||||
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
|
|
||||||
}
|
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct SecretAgent: App {
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
@SceneBuilder var body: some Scene {
|
@MainActor private let storeList: SecretStoreList = {
|
||||||
MenuBarExtra("Test", systemImage: "lock") {
|
let list = SecretStoreList()
|
||||||
AgentStatusView()
|
let cryptoKit = SecureEnclave.Store()
|
||||||
.fixedSize()
|
let migrator = SecureEnclave.CryptoKitMigrator()
|
||||||
|
try? migrator.migrate(to: cryptoKit)
|
||||||
|
list.add(store: cryptoKit)
|
||||||
|
list.add(store: SmartCard.Store())
|
||||||
|
return list
|
||||||
|
}()
|
||||||
|
private let updater = Updater(checkOnLaunch: true)
|
||||||
|
private let notifier = Notifier()
|
||||||
|
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory)
|
||||||
|
private lazy var agent: Agent = {
|
||||||
|
Agent(storeList: storeList, witness: notifier)
|
||||||
|
}()
|
||||||
|
private lazy var socketController: SocketController = {
|
||||||
|
let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
|
||||||
|
return SocketController(path: path)
|
||||||
|
}()
|
||||||
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
|
||||||
|
|
||||||
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
|
logger.debug("SecretAgent finished launching")
|
||||||
|
Task {
|
||||||
|
for await session in socketController.sessions {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
for await message in session.messages {
|
||||||
|
let agentResponse = try await agent.handle(data: message, provenance: session.provenance)
|
||||||
|
try await session.write(agentResponse)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
try session.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task {
|
||||||
|
for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) {
|
||||||
|
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||||
|
notifier.prompt()
|
||||||
|
_ = withObservationTracking {
|
||||||
|
updater.update
|
||||||
|
} onChange: { [updater, notifier] in
|
||||||
|
Task {
|
||||||
|
guard !updater.testBuild else { return }
|
||||||
|
await notifier.notify(update: updater.update!) { release in
|
||||||
|
await updater.ignore(release: release)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.menuBarExtraStyle(.window)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//@main
|
|
||||||
//class AppDelegate: NSObject, NSApplicationDelegate {
|
|
||||||
//
|
|
||||||
// @MainActor private let storeList: SecretStoreList = {
|
|
||||||
// let list = SecretStoreList()
|
|
||||||
// let cryptoKit = SecureEnclave.Store()
|
|
||||||
// let migrator = SecureEnclave.CryptoKitMigrator()
|
|
||||||
// try? migrator.migrate(to: cryptoKit)
|
|
||||||
// list.add(store: cryptoKit)
|
|
||||||
// list.add(store: SmartCard.Store())
|
|
||||||
// return list
|
|
||||||
// }()
|
|
||||||
// private let updater = Updater(checkOnLaunch: true)
|
|
||||||
// private let notifier = Notifier()
|
|
||||||
// private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory)
|
|
||||||
// private lazy var agent: Agent = {
|
|
||||||
// Agent(storeList: storeList, witness: notifier)
|
|
||||||
// }()
|
|
||||||
// private lazy var socketController: SocketController = {
|
|
||||||
// let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
|
|
||||||
// return SocketController(path: path)
|
|
||||||
// }()
|
|
||||||
// private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
|
|
||||||
//
|
|
||||||
// func applicationDidFinishLaunching(_ aNotification: Notification) {
|
|
||||||
// logger.debug("SecretAgent finished launching")
|
|
||||||
// Task {
|
|
||||||
// for await session in socketController.sessions {
|
|
||||||
// Task {
|
|
||||||
// do {
|
|
||||||
// for await message in session.messages {
|
|
||||||
// let agentResponse = try await agent.handle(data: message, provenance: session.provenance)
|
|
||||||
// try await session.write(agentResponse)
|
|
||||||
// }
|
|
||||||
// } catch {
|
|
||||||
// try session.close()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// Task {
|
|
||||||
// for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) {
|
|
||||||
// try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
|
||||||
// notifier.prompt()
|
|
||||||
// _ = withObservationTracking {
|
|
||||||
// updater.update
|
|
||||||
// } onChange: { [updater, notifier] in
|
|
||||||
// Task {
|
|
||||||
// guard !updater.testBuild else { return }
|
|
||||||
// await notifier.notify(update: updater.update!) { release in
|
|
||||||
// await updater.ignore(release: release)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@
|
|||||||
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 */; };
|
||||||
@@ -33,11 +37,6 @@
|
|||||||
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; };
|
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; };
|
||||||
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
|
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
|
||||||
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
|
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
|
||||||
5064ADD32E669B1100B1382C /* AgentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */; };
|
|
||||||
5064ADD42E669B2300B1382C /* AgentStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */; };
|
|
||||||
5064ADD52E669B3000B1382C /* BundleIDs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50033AC227813F1700253856 /* BundleIDs.swift */; };
|
|
||||||
5064ADD62E669B5F00B1382C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
|
|
||||||
5064ADD72E669B7E00B1382C /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; };
|
|
||||||
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
|
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
|
||||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
|
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
|
||||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
|
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
|
||||||
@@ -111,10 +110,14 @@
|
|||||||
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/Localizable.xcstrings; sourceTree = SOURCE_ROOT; };
|
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Resources/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; };
|
||||||
@@ -190,6 +193,55 @@
|
|||||||
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 = (
|
||||||
@@ -252,24 +304,10 @@
|
|||||||
508A58B0241ED1C40069DC07 /* Views */ = {
|
508A58B0241ED1C40069DC07 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
50617D8423FCE48E0099B055 /* ContentView.swift */,
|
504788EF2E681ED700B4556F /* Configuration */,
|
||||||
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
|
504788EE2E681EC300B4556F /* Secrets */,
|
||||||
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
|
504788ED2E681EB200B4556F /* Styles */,
|
||||||
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
|
504788F02E681F0100B4556F /* Views */,
|
||||||
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>";
|
||||||
@@ -277,6 +315,7 @@
|
|||||||
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 */,
|
||||||
@@ -447,12 +486,15 @@
|
|||||||
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 */,
|
||||||
@@ -470,6 +512,7 @@
|
|||||||
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 */,
|
||||||
@@ -481,13 +524,8 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
5064ADD32E669B1100B1382C /* AgentStatusView.swift in Sources */,
|
|
||||||
5064ADD72E669B7E00B1382C /* ConfigurationItemView.swift in Sources */,
|
|
||||||
50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
|
50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
|
||||||
5064ADD52E669B3000B1382C /* BundleIDs.swift in Sources */,
|
|
||||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */,
|
5018F54F24064786002EB505 /* Notifier.swift in Sources */,
|
||||||
5064ADD62E669B5F00B1382C /* LaunchAgentController.swift in Sources */,
|
|
||||||
5064ADD42E669B2300B1382C /* AgentStatusChecker.swift in Sources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
12
Sources/Secretive/Controllers/URLs.swift
Normal file
12
Sources/Secretive/Controllers/URLs.swift
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
179
Sources/Secretive/Views/Configuration/Instructions.swift
Normal file
179
Sources/Secretive/Views/Configuration/Instructions.swift
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
115
Sources/Secretive/Views/Configuration/IntegrationsView.swift
Normal file
115
Sources/Secretive/Views/Configuration/IntegrationsView.swift
Normal file
@@ -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: 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)
|
||||||
|
}
|
||||||
@@ -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: 700, maxWidth: .infinity)
|
.frame(minWidth: 600, maxWidth: .infinity)
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(.setupDoneButton) {
|
Button(.setupDoneButton) {
|
||||||
@@ -154,17 +154,19 @@ struct StepView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 20) {
|
HStack(spacing: 0) {
|
||||||
icon
|
icon
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 24)
|
.frame(width: 24)
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
Spacer()
|
||||||
|
.frame(width: 20)
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.bold()
|
.bold()
|
||||||
Text(description)
|
Text(description)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer(minLength: 20)
|
||||||
actions
|
actions
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,350 +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) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -37,14 +37,6 @@ 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"))
|
||||||
}
|
}
|
||||||
@@ -1,96 +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 {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,6 @@ 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 {
|
||||||
@@ -28,8 +27,8 @@ struct AgentRunningView: View {
|
|||||||
)
|
)
|
||||||
ConfigurationItemView(
|
ConfigurationItemView(
|
||||||
title: .agentDetailsSocketPathTitle,
|
title: .agentDetailsSocketPathTitle,
|
||||||
value: socketPath,
|
value: URL.socketPath,
|
||||||
action: .copy(socketPath),
|
action: .copy(URL.socketPath),
|
||||||
)
|
)
|
||||||
ConfigurationItemView(
|
ConfigurationItemView(
|
||||||
title: .agentDetailsVersionTitle,
|
title: .agentDetailsVersionTitle,
|
||||||
@@ -127,7 +126,7 @@ struct AgentNotRunningView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// .primaryButton()
|
.primaryButton()
|
||||||
} else {
|
} else {
|
||||||
Text(.agentDetailsCouldNotStartError)
|
Text(.agentDetailsCouldNotStartError)
|
||||||
.bold()
|
.bold()
|
||||||
@@ -144,11 +143,11 @@ struct AgentNotRunningView: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//#Preview {
|
#Preview {
|
||||||
// AgentStatusView()
|
AgentStatusView()
|
||||||
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
|
.environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
|
||||||
//}
|
}
|
||||||
//#Preview {
|
#Preview {
|
||||||
// AgentStatusView()
|
AgentStatusView()
|
||||||
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current))
|
.environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current))
|
||||||
//}
|
}
|
||||||
63
Sources/Secretive/Views/Views/UpdateView.swift
Normal file
63
Sources/Secretive/Views/Views/UpdateView.swift
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user