mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-04-10 03:07:22 +02:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6f5e98302 | ||
|
|
c352ed4cc9 | ||
|
|
ea71993801 | ||
|
|
df2b7881c4 | ||
|
|
74ddb9595b | ||
|
|
0980cdffcd | ||
|
|
90d55726bb | ||
|
|
a640d11b00 | ||
|
|
f3ce6b9d0f | ||
|
|
ea96dd88eb | ||
|
|
4d84621b3d | ||
|
|
2d05a7b0f3 | ||
|
|
c8d90ba455 | ||
|
|
9299bf343f | ||
|
|
fa658646d7 | ||
|
|
cd76bb95ec | ||
|
|
b949d846c1 | ||
|
|
19760f1e02 | ||
|
|
f60a44c599 | ||
|
|
260e63341d | ||
|
|
cbf903deb7 |
23
.github/workflows/nightly.yml
vendored
23
.github/workflows/nightly.yml
vendored
@@ -3,16 +3,10 @@ name: Nightly
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 8 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# runs-on: macOS-latest
|
||||
runs-on: macos-15
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
attestations: write
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
@@ -36,23 +30,22 @@ jobs:
|
||||
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
|
||||
- name: Build
|
||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||
- name: Create ZIP
|
||||
- name: Create ZIPs
|
||||
run: |
|
||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
|
||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Archive.zip
|
||||
- name: Notarize
|
||||
env:
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
||||
- name: Upload App to Artifacts
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Secretive.zip
|
||||
path: Secretive.zip
|
||||
- name: Attest
|
||||
id: attest
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: "Secretive.zip"
|
||||
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }}
|
||||
subject-path: 'Secretive.zip'
|
||||
- name: Upload App to Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Secretive.zip
|
||||
path: Secretive.zip
|
||||
|
||||
23
.github/workflows/release.yml
vendored
23
.github/workflows/release.yml
vendored
@@ -56,34 +56,39 @@ jobs:
|
||||
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
|
||||
- name: Build
|
||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||
- name: Create ZIP
|
||||
- name: Create ZIPs
|
||||
run: |
|
||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
|
||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Xcode_Archive.zip
|
||||
- name: Notarize
|
||||
env:
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
||||
- name: Upload App to Artifacts
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Secretive.zip
|
||||
path: Secretive.zip
|
||||
- name: Attest
|
||||
id: attest
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: "Secretive.zip"
|
||||
subject-digest: ${{ steps.upload.outputs.artifact-digest }}
|
||||
subject-path: 'Secretive.zip, Xcode_Archive.zip'
|
||||
- name: Create Release
|
||||
run: |
|
||||
sed -i.tmp "s/RUN_ID/$RUN_ID/g" .github/templates/release.md
|
||||
sed -i.tmp "s/ATTESTATION_ID/$ATTESTATION_ID/g" .github/templates/release.md
|
||||
gh release create $TAG_NAME -d -F .github/templates/release.md
|
||||
gh release upload Secretive.zip
|
||||
gh release upload Xcode_Archive.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ github.ref }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
ATTESTATION_ID: ${{ steps.attest.outputs.attestation-id }}
|
||||
- name: Upload App to Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Secretive.zip
|
||||
path: Secretive.zip
|
||||
- name: Upload Archive to Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Xcode_Archive.zip
|
||||
path: Xcode_Archive.zip
|
||||
|
||||
@@ -57,7 +57,7 @@ let package = Package(
|
||||
)
|
||||
|
||||
var localization: Resource {
|
||||
.process("../../Resources/Localizable.xcstrings")
|
||||
.process("../../Localizable.xcstrings")
|
||||
}
|
||||
|
||||
var swiftSettings: [PackageDescription.SwiftSetting] {
|
||||
|
||||
@@ -2983,73 +2983,73 @@
|
||||
"localizations" : {
|
||||
"ca" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "Secretive suporta claus EC256, EC384, RSA1024 i RSA2048."
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "Secretive unterstützt EC256, EC384, RSA1024 und RSA2048 Schlüssel."
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Secretive supports EC256, EC384, and RSA2048 keys."
|
||||
"value" : "Secretive supports EC256, EC384, RSA1024, and RSA2048 keys."
|
||||
}
|
||||
},
|
||||
"fi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "Secretive tukee EC256-, EC384-, RSA1024- ja RSA2048-avaimia."
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "Secretive prend en charge les clés EC256, EC384, RSA1024 et RSA2048."
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "Secretive supporta la cifratura EC256, EC384, RSA1024 e RSA2048."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "SecretiveはEC256、EC384、RSA1024、またはRSA2048の鍵に対応しています。"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "Secretive는 EC256, EC384, RSA1024 및 RSA2048 키를 지원합니다."
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "Secretive wspiera klucze EC256, EC384, RSA1024 i RSA2048."
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "Secretive suporta chaves EC256, EC384, RSA1024 e RSA2048."
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "Secretive поддерживает ключи EC256, EC384, RSA1024, и RSA2048."
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"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" : {
|
||||
"extractionState" : "manual",
|
||||
"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" : {
|
||||
"extractionState" : "manual",
|
||||
"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" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -3414,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" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -3436,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" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -3517,13 +3363,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"integrationsGitStepGitconfigSectionNote" : {
|
||||
"integrationsMenuBarTitle" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "If any section (like [user]) already exists, just add the entries in the existing section."
|
||||
"value" : "Integrations…"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4423,8 +4269,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"set -x SSH_AUTH_SOCK %@" : {
|
||||
"shouldTranslate" : false
|
||||
"Setup" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Setup"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"setup_agent_activity_monitor_description" : {
|
||||
"extractionState" : "manual",
|
||||
@@ -5346,6 +5200,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Test" : {
|
||||
|
||||
},
|
||||
"unnamed_secret" : {
|
||||
"extractionState" : "manual",
|
||||
@@ -82,7 +82,7 @@ let package = Package(
|
||||
)
|
||||
|
||||
var localization: Resource {
|
||||
.process("../../Resources/Localizable.xcstrings")
|
||||
.process("../../Localizable.xcstrings")
|
||||
}
|
||||
|
||||
var swiftSettings: [PackageDescription.SwiftSetting] {
|
||||
|
||||
1
Sources/Packages/Sources/Localization/Stub.swift
Normal file
1
Sources/Packages/Sources/Localization/Stub.swift
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -26,8 +26,7 @@ public final class PublicKeyFileStoreController: Sendable {
|
||||
let untracked = Set(fullPathContents)
|
||||
.subtracting(validPaths)
|
||||
for path in untracked {
|
||||
// string instead of fileURLWithPath since we're already using fileURL format.
|
||||
try? FileManager.default.removeItem(at: URL(string: path)!)
|
||||
try? FileManager.default.removeItem(at: URL(fileURLWithPath: path))
|
||||
}
|
||||
}
|
||||
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil)
|
||||
|
||||
@@ -30,7 +30,7 @@ extension SecureEnclave {
|
||||
SecItemCopyMatching(privateAttributes, &privateUntyped)
|
||||
guard let privateTyped = privateUntyped as? [[CFString: Any]] else { return }
|
||||
let migratedPublicKeys = Set(store.secrets.map(\.publicKey))
|
||||
var migratedAny = false
|
||||
var migrated = false
|
||||
for key in privateTyped {
|
||||
let name = key[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret)
|
||||
let id = key[kSecAttrApplicationLabel] as! Data
|
||||
@@ -45,7 +45,6 @@ extension SecureEnclave {
|
||||
// Best guess.
|
||||
let auth: AuthenticationRequirement = String(describing: accessControl)
|
||||
.contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown
|
||||
do {
|
||||
let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID)
|
||||
let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth))
|
||||
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
|
||||
@@ -57,12 +56,9 @@ extension SecureEnclave {
|
||||
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
|
||||
logger.log("Migrated \(name).")
|
||||
try markMigrated(secret: secret, oldID: id)
|
||||
migratedAny = true
|
||||
} catch {
|
||||
logger.error("Failed to migrate \(name): \(error).")
|
||||
migrated = true
|
||||
}
|
||||
}
|
||||
if migratedAny {
|
||||
if migrated {
|
||||
store.reloadSecrets()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ extension SecureEnclave {
|
||||
for await note in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) {
|
||||
guard Constants.notificationToken != (note.object as? String) else {
|
||||
// Don't reload if we're the ones triggering this by reloading.
|
||||
continue
|
||||
return
|
||||
}
|
||||
reloadSecrets()
|
||||
}
|
||||
|
||||
@@ -6,65 +6,85 @@ import SmartCardSecretKit
|
||||
import SecretAgentKit
|
||||
import Brief
|
||||
import Observation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
extension EnvironmentValues {
|
||||
private static let _agentStatusChecker = AgentStatusChecker()
|
||||
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
|
||||
}
|
||||
|
||||
@main
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
struct SecretAgent: App {
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
@SceneBuilder var body: some Scene {
|
||||
MenuBarExtra("Test", systemImage: "lock") {
|
||||
AgentStatusView()
|
||||
.fixedSize()
|
||||
}
|
||||
.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,10 +26,6 @@
|
||||
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
|
||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
||||
504788EC2E680DC800B4556F /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788EB2E680DC400B4556F /* URLs.swift */; };
|
||||
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
|
||||
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; };
|
||||
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
|
||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
|
||||
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
|
||||
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
|
||||
@@ -37,6 +33,11 @@
|
||||
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* 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 */; };
|
||||
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 */; };
|
||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
|
||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
|
||||
@@ -110,14 +111,10 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
@@ -193,55 +190,6 @@
|
||||
path = Helpers;
|
||||
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 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -304,10 +252,24 @@
|
||||
508A58B0241ED1C40069DC07 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504788EF2E681ED700B4556F /* Configuration */,
|
||||
504788EE2E681EC300B4556F /* Secrets */,
|
||||
504788ED2E681EB200B4556F /* Styles */,
|
||||
504788F02E681F0100B4556F /* Views */,
|
||||
50617D8423FCE48E0099B055 /* ContentView.swift */,
|
||||
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
|
||||
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
|
||||
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
|
||||
50153E21250DECA300525160 /* SecretListItemView.swift */,
|
||||
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
|
||||
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
|
||||
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
|
||||
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */,
|
||||
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
|
||||
506772C82425BB8500034DED /* NoStoresView.swift */,
|
||||
50153E1F250AFCB200525160 /* UpdateView.swift */,
|
||||
5066A6C12516F303004B5A36 /* SetupView.swift */,
|
||||
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
|
||||
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */,
|
||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
||||
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
|
||||
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@@ -315,7 +277,6 @@
|
||||
508A58B1241ED1EA0069DC07 /* Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504788EB2E680DC400B4556F /* URLs.swift */,
|
||||
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
|
||||
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
|
||||
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
|
||||
@@ -486,15 +447,12 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504788F22E681F3A00B4556F /* Instructions.swift in Sources */,
|
||||
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */,
|
||||
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
|
||||
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
||||
504788EC2E680DC800B4556F /* URLs.swift in Sources */,
|
||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
||||
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
|
||||
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
|
||||
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */,
|
||||
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */,
|
||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
|
||||
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
|
||||
@@ -512,7 +470,6 @@
|
||||
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
|
||||
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */,
|
||||
50617D8323FCE48E0099B055 /* App.swift in Sources */,
|
||||
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */,
|
||||
506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
|
||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
|
||||
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
|
||||
@@ -524,8 +481,13 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5064ADD32E669B1100B1382C /* AgentStatusView.swift in Sources */,
|
||||
5064ADD72E669B7E00B1382C /* ConfigurationItemView.swift in Sources */,
|
||||
50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
|
||||
5064ADD52E669B3000B1382C /* BundleIDs.swift in Sources */,
|
||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */,
|
||||
5064ADD62E669B5F00B1382C /* LaunchAgentController.swift in Sources */,
|
||||
5064ADD42E669B2300B1382C /* AgentStatusChecker.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ struct AgentStatusView: View {
|
||||
struct AgentRunningView: View {
|
||||
|
||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||
private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
@@ -27,8 +28,8 @@ struct AgentRunningView: View {
|
||||
)
|
||||
ConfigurationItemView(
|
||||
title: .agentDetailsSocketPathTitle,
|
||||
value: URL.socketPath,
|
||||
action: .copy(URL.socketPath),
|
||||
value: socketPath,
|
||||
action: .copy(socketPath),
|
||||
)
|
||||
ConfigurationItemView(
|
||||
title: .agentDetailsVersionTitle,
|
||||
@@ -126,7 +127,7 @@ struct AgentNotRunningView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.primaryButton()
|
||||
// .primaryButton()
|
||||
} else {
|
||||
Text(.agentDetailsCouldNotStartError)
|
||||
.bold()
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -198,17 +198,25 @@ extension ContentView {
|
||||
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
// Empty on modifiable and nonmodifiable
|
||||
ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
|
||||
.environment(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
|
||||
.environment(PreviewUpdater())
|
||||
|
||||
// 5 items on modifiable and nonmodifiable
|
||||
ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
|
||||
.environment(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
|
||||
.environment(PreviewUpdater())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
//#Preview {
|
||||
// // Empty on modifiable and nonmodifiable
|
||||
// ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
|
||||
// .environment(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
|
||||
// .environment(PreviewUpdater())
|
||||
//}
|
||||
//
|
||||
//#Preview {
|
||||
// // 5 items on modifiable and nonmodifiable
|
||||
// ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
|
||||
// .environment(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
|
||||
// .environment(PreviewUpdater())
|
||||
//}
|
||||
@@ -163,12 +163,17 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
#if DEBUG
|
||||
|
||||
struct CopyableView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
|
||||
.padding()
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -146,6 +146,6 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// CreateSecretView(store: Preview.StoreModifiable()) { _ in }
|
||||
//}
|
||||
#Preview {
|
||||
CreateSecretView(store: Preview.StoreModifiable()) { _ in }
|
||||
}
|
||||
@@ -57,10 +57,15 @@ struct EmptyStoreModifiableView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
#Preview {
|
||||
struct EmptyStoreModifiableView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
EmptyStoreImmutableView()
|
||||
}
|
||||
#Preview {
|
||||
EmptyStoreModifiableView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
350
Sources/Secretive/Views/IntegrationsView.swift
Normal file
350
Sources/Secretive/Views/IntegrationsView.swift
Normal 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)
|
||||
}
|
||||
@@ -13,7 +13,12 @@ struct NoStoresView: View {
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
#if DEBUG
|
||||
|
||||
struct NoStoresView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NoStoresView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -37,6 +37,14 @@ struct SecretDetailView<SecretType: Secret>: View {
|
||||
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret"))
|
||||
//}
|
||||
extension URL {
|
||||
|
||||
static var agentHomeURL: URL {
|
||||
URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret"))
|
||||
}
|
||||
@@ -67,7 +67,7 @@ struct SetupView: View {
|
||||
buttonWidth = width
|
||||
}
|
||||
.background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
|
||||
.frame(minWidth: 600, maxWidth: .infinity)
|
||||
.frame(minWidth: 700, maxWidth: .infinity)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(.setupDoneButton) {
|
||||
@@ -154,19 +154,17 @@ struct StepView<Content: View>: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
HStack(spacing: 20) {
|
||||
icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 24)
|
||||
Spacer()
|
||||
.frame(width: 20)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(title)
|
||||
.bold()
|
||||
Text(description)
|
||||
}
|
||||
Spacer(minLength: 20)
|
||||
Spacer()
|
||||
actions
|
||||
}
|
||||
.padding(20)
|
||||
96
Sources/Secretive/Views/UpdateView.swift
Normal file
96
Sources/Secretive/Views/UpdateView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user