mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-04-09 18:57:22 +02:00
Compare commits
3 Commits
log_err
...
codeql_wor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8826726bc3 | ||
|
|
bf78879310 | ||
|
|
ca495f4b7f |
46
.github/workflows/codeql.yml
vendored
Normal file
46
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '26 15 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
security-events: write
|
||||
packages: read
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
- language: swift
|
||||
build-mode: manual
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
- if: matrix.build-mode == 'manual'
|
||||
name: "Select Xcode"
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
|
||||
- if: matrix.build-mode == 'manual'
|
||||
name: "Build"
|
||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
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] {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 @@
|
||||
|
||||
@@ -4,7 +4,7 @@ import OSLog
|
||||
/// Manages storage and lookup for OpenSSH certificates.
|
||||
public actor OpenSSHCertificateHandler: Sendable {
|
||||
|
||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory)
|
||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
|
||||
private let writer = OpenSSHPublicKeyWriter()
|
||||
private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:]
|
||||
|
||||
@@ -5,12 +5,12 @@ import OSLog
|
||||
public final class PublicKeyFileStoreController: Sendable {
|
||||
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
|
||||
private let directory: URL
|
||||
private let directory: String
|
||||
private let keyWriter = OpenSSHPublicKeyWriter()
|
||||
|
||||
/// Initializes a PublicKeyFileStoreController.
|
||||
public init(homeDirectory: URL) {
|
||||
directory = homeDirectory.appending(component: "PublicKeys")
|
||||
public init(homeDirectory: String) {
|
||||
directory = homeDirectory.appending("/PublicKeys")
|
||||
}
|
||||
|
||||
/// Writes out the keys specified to disk.
|
||||
@@ -20,17 +20,16 @@ public final class PublicKeyFileStoreController: Sendable {
|
||||
logger.log("Writing public keys to disk")
|
||||
if clear {
|
||||
let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) }))
|
||||
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory.path())) ?? []
|
||||
let contentsOfDirectory = (try? FileManager.default.contentsOfDirectory(atPath: directory)) ?? []
|
||||
let fullPathContents = contentsOfDirectory.map { "\(directory)/\($0)" }
|
||||
|
||||
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)
|
||||
try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil)
|
||||
for secret in secrets {
|
||||
let path = publicKeyPath(for: secret)
|
||||
let data = Data(keyWriter.openSSHString(secret: secret).utf8)
|
||||
@@ -45,14 +44,14 @@ public final class PublicKeyFileStoreController: Sendable {
|
||||
/// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to.
|
||||
public func publicKeyPath<SecretType: Secret>(for secret: SecretType) -> String {
|
||||
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
||||
return directory.appending(component: "\(minimalHex).pub").path()
|
||||
return directory.appending("/").appending("\(minimalHex).pub")
|
||||
}
|
||||
|
||||
/// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory.
|
||||
public var hasAnyCertificates: Bool {
|
||||
do {
|
||||
return try FileManager.default
|
||||
.contentsOfDirectory(atPath: directory.path())
|
||||
.contentsOfDirectory(atPath: directory)
|
||||
.filter { $0.hasSuffix("-cert.pub") }
|
||||
.isEmpty == false
|
||||
} catch {
|
||||
@@ -66,7 +65,7 @@ public final class PublicKeyFileStoreController: Sendable {
|
||||
/// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be.
|
||||
public func sshCertificatePath<SecretType: Secret>(for secret: SecretType) -> String {
|
||||
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
|
||||
return directory.appending(component: "\(minimalHex)-cert.pub").path()
|
||||
return directory.appending("/").appending("\(minimalHex)-cert.pub")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,24 +45,20 @@ 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 {
|
||||
logger.log("Skipping \(name), public key already present. Marking as migrated.")
|
||||
try markMigrated(secret: secret, oldID: id)
|
||||
continue
|
||||
}
|
||||
logger.log("Migrating \(name).")
|
||||
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
|
||||
logger.log("Migrated \(name).")
|
||||
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 {
|
||||
logger.log("Skipping \(name), public key already present. Marking as migrated.")
|
||||
try markMigrated(secret: secret, oldID: id)
|
||||
migratedAny = true
|
||||
} catch {
|
||||
logger.error("Failed to migrate \(name): \(error).")
|
||||
continue
|
||||
}
|
||||
logger.log("Migrating \(name).")
|
||||
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
|
||||
logger.log("Migrated \(name).")
|
||||
try markMigrated(secret: secret, oldID: id)
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
}()
|
||||
private let updater = Updater(checkOnLaunch: true)
|
||||
private let notifier = Notifier()
|
||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory)
|
||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
||||
private lazy var agent: Agent = {
|
||||
Agent(storeList: storeList, witness: notifier)
|
||||
}()
|
||||
|
||||
@@ -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 */; };
|
||||
@@ -40,6 +36,7 @@
|
||||
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
|
||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
|
||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
|
||||
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; };
|
||||
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
|
||||
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
|
||||
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; };
|
||||
@@ -52,12 +49,8 @@
|
||||
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; };
|
||||
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79324026B7600D209EA /* Preview Assets.xcassets */; };
|
||||
50A3B79724026B7600D209EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79524026B7600D209EA /* Main.storyboard */; };
|
||||
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */; };
|
||||
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; };
|
||||
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; };
|
||||
50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */; };
|
||||
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */; };
|
||||
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; };
|
||||
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
|
||||
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -110,14 +103,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; };
|
||||
@@ -131,6 +120,7 @@
|
||||
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = "<group>"; };
|
||||
5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
|
||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; };
|
||||
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = "<group>"; };
|
||||
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
|
||||
506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; };
|
||||
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = "<group>"; };
|
||||
@@ -148,12 +138,8 @@
|
||||
50A3B79624026B7600D209EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
50A3B79824026B7600D209EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50A3B79924026B7600D209EA /* SecretAgent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecretAgent.entitlements; sourceTree = "<group>"; };
|
||||
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationsView.swift; sourceTree = "<group>"; };
|
||||
50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = "<group>"; };
|
||||
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; };
|
||||
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusView.swift; sourceTree = "<group>"; };
|
||||
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorStyle.swift; sourceTree = "<group>"; };
|
||||
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = "<group>"; };
|
||||
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
|
||||
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -193,55 +179,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 +241,20 @@
|
||||
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 */,
|
||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@@ -315,11 +262,11 @@
|
||||
508A58B1241ED1EA0069DC07 /* Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504788EB2E680DC400B4556F /* URLs.swift */,
|
||||
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
|
||||
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
|
||||
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
|
||||
50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
|
||||
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */,
|
||||
);
|
||||
path = Controllers;
|
||||
sourceTree = "<group>";
|
||||
@@ -486,33 +433,26 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504788F22E681F3A00B4556F /* Instructions.swift in Sources */,
|
||||
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */,
|
||||
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
|
||||
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
||||
504788EC2E680DC800B4556F /* URLs.swift in Sources */,
|
||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
||||
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
|
||||
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
|
||||
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */,
|
||||
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */,
|
||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
|
||||
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
|
||||
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
|
||||
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */,
|
||||
50033AC327813F1700253856 /* BundleIDs.swift in Sources */,
|
||||
50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */,
|
||||
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
|
||||
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
|
||||
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
|
||||
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
|
||||
50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
|
||||
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */,
|
||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
|
||||
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
|
||||
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
|
||||
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */,
|
||||
50617D8323FCE48E0099B055 /* App.swift in Sources */,
|
||||
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */,
|
||||
506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
|
||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
|
||||
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
|
||||
@@ -707,18 +647,10 @@
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_ENHANCED_SECURITY = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
||||
ENABLE_USER_SELECTED_FILES = readwrite;
|
||||
INFOPLIST_FILE = Secretive/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -747,18 +679,10 @@
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_ENHANCED_SECURITY = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
||||
ENABLE_USER_SELECTED_FILES = readwrite;
|
||||
INFOPLIST_FILE = Secretive/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -859,18 +783,10 @@
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_ENHANCED_SECURITY = YES;
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_POINTER_AUTHENTICATION = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
||||
ENABLE_USER_SELECTED_FILES = readwrite;
|
||||
INFOPLIST_FILE = Secretive/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -893,17 +809,8 @@
|
||||
DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\"";
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
||||
INFOPLIST_FILE = SecretAgent/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -928,17 +835,8 @@
|
||||
DEVELOPMENT_TEAM = Z72PRUAWF6;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
||||
INFOPLIST_FILE = SecretAgent/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -964,17 +862,8 @@
|
||||
DEVELOPMENT_TEAM = Z72PRUAWF6;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
|
||||
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
|
||||
ENABLE_RESOURCE_ACCESS_USB = NO;
|
||||
INFOPLIST_FILE = SecretAgent/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
||||
@@ -37,7 +37,6 @@ struct Secretive: App {
|
||||
@Environment(\.agentStatusChecker) var agentStatusChecker
|
||||
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
|
||||
@State private var showingSetup = false
|
||||
@State private var showingIntegrations = false
|
||||
@State private var showingCreation = false
|
||||
|
||||
@SceneBuilder var body: some Scene {
|
||||
@@ -59,16 +58,8 @@ struct Secretive: App {
|
||||
forceLaunchAgent()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingIntegrations) {
|
||||
IntegrationsView()
|
||||
}
|
||||
}
|
||||
.commands {
|
||||
CommandGroup(before: CommandGroupPlacement.appSettings) {
|
||||
Button(.integrationsMenuBarTitle, systemImage: "app.connected.to.app.below.fill") {
|
||||
showingIntegrations = true
|
||||
}
|
||||
}
|
||||
CommandGroup(after: CommandGroupPlacement.newItem) {
|
||||
Button(.appMenuNewSecretButton) {
|
||||
showingCreation = true
|
||||
@@ -80,6 +71,11 @@ struct Secretive: App {
|
||||
NSWorkspace.shared.open(Constants.helpURL)
|
||||
}
|
||||
}
|
||||
CommandGroup(after: .help) {
|
||||
Button(.appMenuSetupButton) {
|
||||
showingSetup = true
|
||||
}
|
||||
}
|
||||
SidebarCommands()
|
||||
}
|
||||
}
|
||||
@@ -91,7 +87,7 @@ extension Secretive {
|
||||
private func reinstallAgent() {
|
||||
justUpdatedChecker.check()
|
||||
Task {
|
||||
_ = await LaunchAgentController().install()
|
||||
await LaunchAgentController().install()
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
agentStatusChecker.check()
|
||||
if !agentStatusChecker.running {
|
||||
|
||||
@@ -6,14 +6,12 @@ import Observation
|
||||
@MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable {
|
||||
var running: Bool { get }
|
||||
var developmentBuild: Bool { get }
|
||||
var process: NSRunningApplication? { get }
|
||||
func check()
|
||||
}
|
||||
|
||||
@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol {
|
||||
|
||||
var running: Bool = false
|
||||
var process: NSRunningApplication? = nil
|
||||
|
||||
nonisolated init() {
|
||||
Task { @MainActor in
|
||||
@@ -22,39 +20,32 @@ import Observation
|
||||
}
|
||||
|
||||
func check() {
|
||||
process = instanceSecretAgentProcess
|
||||
running = process != nil
|
||||
running = instanceSecretAgentProcess != nil
|
||||
}
|
||||
|
||||
// All processes, including ones from older versions, etc
|
||||
var allSecretAgentProcesses: [NSRunningApplication] {
|
||||
NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.agentBundleID)
|
||||
var secretAgentProcesses: [NSRunningApplication] {
|
||||
NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.agentBundleID)
|
||||
}
|
||||
|
||||
// The process corresponding to this instance of Secretive
|
||||
var instanceSecretAgentProcess: NSRunningApplication? {
|
||||
// FIXME: CHECK VERSION
|
||||
let agents = allSecretAgentProcesses
|
||||
let agents = secretAgentProcesses
|
||||
for agent in agents {
|
||||
guard let url = agent.bundleURL else { continue }
|
||||
if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) || (url.isXcodeURL && developmentBuild) {
|
||||
if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) {
|
||||
return agent
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// Whether Secretive is being run in an Xcode environment.
|
||||
var developmentBuild: Bool {
|
||||
Bundle.main.bundleURL.isXcodeURL
|
||||
Bundle.main.bundleURL.absoluteString.contains("/Library/Developer/Xcode")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension URL {
|
||||
|
||||
var isXcodeURL: Bool {
|
||||
absoluteString.contains("/Library/Developer/Xcode")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,28 +8,16 @@ struct LaunchAgentController {
|
||||
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
|
||||
|
||||
func install() async -> Bool {
|
||||
func install() async {
|
||||
logger.debug("Installing agent")
|
||||
_ = setEnabled(false)
|
||||
// This is definitely a bit of a "seems to work better" thing but:
|
||||
// Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
|
||||
// and start new?
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
let result = await MainActor.run {
|
||||
setEnabled(true)
|
||||
await MainActor.run {
|
||||
_ = setEnabled(true)
|
||||
}
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
return result
|
||||
}
|
||||
|
||||
func uninstall() async -> Bool {
|
||||
logger.debug("Uninstalling agent")
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
let result = await MainActor.run {
|
||||
setEnabled(false)
|
||||
}
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
return result
|
||||
}
|
||||
|
||||
func forceLaunch() async -> Bool {
|
||||
@@ -40,7 +28,6 @@ struct LaunchAgentController {
|
||||
do {
|
||||
try await NSWorkspace.shared.openApplication(at: url, configuration: config)
|
||||
logger.debug("Agent force launched")
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
return true
|
||||
} catch {
|
||||
logger.error("Error force launching \(error.localizedDescription)")
|
||||
@@ -49,7 +36,7 @@ struct LaunchAgentController {
|
||||
}
|
||||
|
||||
private func setEnabled(_ enabled: Bool) -> Bool {
|
||||
let service = SMAppService.loginItem(identifier: Bundle.agentBundleID)
|
||||
let service = SMAppService.loginItem(identifier: Bundle.main.agentBundleID)
|
||||
do {
|
||||
if enabled {
|
||||
try service.register()
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
import SecretKit
|
||||
|
||||
struct ShellConfigurationController {
|
||||
|
||||
let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
|
||||
|
||||
var shellInstructions: [ShellConfigInstruction] {
|
||||
[
|
||||
ShellConfigInstruction(shell: "global",
|
||||
shellConfigDirectory: "~/.ssh/",
|
||||
shellConfigFilename: "config",
|
||||
text: "Host *\n\tIdentityAgent \(socketPath)"),
|
||||
ShellConfigInstruction(shell: "zsh",
|
||||
shellConfigDirectory: "~/",
|
||||
shellConfigFilename: ".zshrc",
|
||||
text: "export SSH_AUTH_SOCK=\(socketPath)"),
|
||||
ShellConfigInstruction(shell: "bash",
|
||||
shellConfigDirectory: "~/",
|
||||
shellConfigFilename: ".bashrc",
|
||||
text: "export SSH_AUTH_SOCK=\(socketPath)"),
|
||||
ShellConfigInstruction(shell: "fish",
|
||||
shellConfigDirectory: "~/.config/fish",
|
||||
shellConfigFilename: "config.fish",
|
||||
text: "set -x SSH_AUTH_SOCK \(socketPath)"),
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
|
||||
@MainActor func addToShell(shellInstructions: ShellConfigInstruction) -> Bool {
|
||||
let openPanel = NSOpenPanel()
|
||||
// This is sync, so no need to strongly retain
|
||||
let delegate = Delegate(name: shellInstructions.shellConfigFilename)
|
||||
openPanel.delegate = delegate
|
||||
openPanel.message = "Select \(shellInstructions.shellConfigFilename) to let Secretive configure your shell automatically."
|
||||
openPanel.prompt = "Add to \(shellInstructions.shellConfigFilename)"
|
||||
openPanel.canChooseFiles = true
|
||||
openPanel.canChooseDirectories = false
|
||||
openPanel.showsHiddenFiles = true
|
||||
openPanel.directoryURL = URL(fileURLWithPath: shellInstructions.shellConfigDirectory)
|
||||
openPanel.nameFieldStringValue = shellInstructions.shellConfigFilename
|
||||
openPanel.allowedContentTypes = [.symbolicLink, .data, .plainText]
|
||||
openPanel.runModal()
|
||||
guard let fileURL = openPanel.urls.first else { return false }
|
||||
let handle: FileHandle
|
||||
do {
|
||||
handle = try FileHandle(forUpdating: fileURL)
|
||||
guard let existing = try handle.readToEnd(),
|
||||
let existingString = String(data: existing, encoding: .utf8) else { return false }
|
||||
guard !existingString.contains(shellInstructions.text) else {
|
||||
return true
|
||||
}
|
||||
try handle.seekToEnd()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
handle.write(Data("\n# Secretive Config\n\(shellInstructions.text)\n".utf8))
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
extension Bundle {
|
||||
public static var agentBundleID: String {
|
||||
Bundle.main.bundleIdentifier!.replacingOccurrences(of: "Host", with: "SecretAgent")
|
||||
}
|
||||
|
||||
public static var hostBundleID: String {
|
||||
Bundle.main.bundleIdentifier!.replacingOccurrences(of: "SecretAgent", with: "Host")
|
||||
}
|
||||
extension Bundle {
|
||||
public var agentBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "Host", with: "SecretAgent"))!}
|
||||
public var hostBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "SecretAgent", with: "Host"))!}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
|
||||
|
||||
let running: Bool
|
||||
let process: NSRunningApplication?
|
||||
let developmentBuild = false
|
||||
|
||||
init(running: Bool = true, process: NSRunningApplication? = nil) {
|
||||
init(running: Bool = true) {
|
||||
self.running = running
|
||||
self.process = process
|
||||
}
|
||||
|
||||
func check() {
|
||||
|
||||
24
Sources/Secretive/Views/ActionButtonStyle.swift
Normal file
24
Sources/Secretive/Views/ActionButtonStyle.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
import SwiftUI
|
||||
|
||||
struct PrimaryButtonModifier: ViewModifier {
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
// Tinted glass prominent is really hard to read on 26.0.
|
||||
if #available(macOS 26.0, *), colorScheme == .dark {
|
||||
content.buttonStyle(.glassProminent)
|
||||
} else {
|
||||
content.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension View {
|
||||
|
||||
func primary() -> some View {
|
||||
modifier(PrimaryButtonModifier())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ConfigurationItemView<Content: View>: View {
|
||||
|
||||
enum Action: Hashable {
|
||||
case copy(String)
|
||||
case revealInFinder(String)
|
||||
}
|
||||
|
||||
let title: LocalizedStringResource
|
||||
let content: Content
|
||||
let action: Action?
|
||||
|
||||
init(title: LocalizedStringResource, value: String, action: Action? = nil) where Content == Text {
|
||||
self.title = title
|
||||
self.content = Text(value)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
self.action = action
|
||||
}
|
||||
|
||||
init(title: LocalizedStringResource, action: Action? = nil, content: () -> Content) {
|
||||
self.title = title
|
||||
self.content = content()
|
||||
self.action = action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(title)
|
||||
Spacer()
|
||||
switch action {
|
||||
case .copy(let string):
|
||||
Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
|
||||
NSPasteboard.general.declareTypes([.string], owner: nil)
|
||||
NSPasteboard.general.setString(string, forType: .string)
|
||||
}
|
||||
.labelStyle(.iconOnly)
|
||||
.buttonStyle(.borderless)
|
||||
case .revealInFinder(let rawPath):
|
||||
Button(.revealInFinderButton, systemImage: "folder") {
|
||||
// All foundation-based normalization methods replace this with the container directly.
|
||||
let processedPath = rawPath.replacingOccurrences(of: "~", with: "/Users/\(NSUserName())")
|
||||
let url = URL(filePath: processedPath)
|
||||
let folder = url.deletingLastPathComponent().path()
|
||||
NSWorkspace.shared.selectFile(processedPath, inFileViewerRootedAtPath: folder)
|
||||
}
|
||||
.labelStyle(.iconOnly)
|
||||
.buttonStyle(.borderless)
|
||||
case nil:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,187 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SetupView: View {
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Binding var setupComplete: Bool
|
||||
|
||||
@State var showingIntegrations = false
|
||||
@State var buttonWidth: CGFloat?
|
||||
|
||||
@State var installed = false
|
||||
@State var updates = false
|
||||
@State var integrations = false
|
||||
var allDone: Bool {
|
||||
installed && updates && integrations
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
StepView(
|
||||
title: .setupAgentTitle,
|
||||
description: .setupAgentDescription,
|
||||
systemImage: "lock.laptopcomputer",
|
||||
) {
|
||||
setupButton(
|
||||
.setupAgentInstallButton,
|
||||
complete: installed,
|
||||
width: buttonWidth
|
||||
) {
|
||||
installed = true
|
||||
Task {
|
||||
await LaunchAgentController().install()
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
StepView(
|
||||
title: .setupUpdatesTitle,
|
||||
description: .setupUpdatesDescription,
|
||||
systemImage: "network.badge.shield.half.filled",
|
||||
) {
|
||||
setupButton(
|
||||
.setupUpdatesOkButton,
|
||||
complete: updates,
|
||||
width: buttonWidth
|
||||
) {
|
||||
updates = true
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
StepView(
|
||||
title: .setupIntegrationsTitle,
|
||||
description: .setupIntegrationsDescription,
|
||||
systemImage: "firewall",
|
||||
) {
|
||||
setupButton(
|
||||
.setupIntegrationsButton,
|
||||
complete: integrations,
|
||||
width: buttonWidth
|
||||
) {
|
||||
showingIntegrations = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.onPreferenceChange(setupButton.WidthKey.self) { width in
|
||||
buttonWidth = width
|
||||
}
|
||||
.background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
|
||||
.frame(minWidth: 600, maxWidth: .infinity)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(.setupDoneButton) {
|
||||
setupComplete = true
|
||||
dismiss()
|
||||
}
|
||||
.disabled(!allDone)
|
||||
.primaryButton()
|
||||
}
|
||||
}
|
||||
.interactiveDismissDisabled()
|
||||
.padding()
|
||||
.sheet(isPresented: $showingIntegrations, onDismiss: {
|
||||
integrations = true
|
||||
}, content: {
|
||||
IntegrationsView()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct setupButton: View {
|
||||
|
||||
struct WidthKey: @MainActor PreferenceKey {
|
||||
@MainActor static var defaultValue: CGFloat? = nil
|
||||
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
|
||||
if let next = nextValue(), next > (value ?? -1) {
|
||||
value = next
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let label: LocalizedStringResource
|
||||
let complete: Bool
|
||||
let action: () -> Void
|
||||
let width: CGFloat?
|
||||
@State var currentWidth: CGFloat?
|
||||
|
||||
init(_ label: LocalizedStringResource, complete: Bool, width: CGFloat? = nil, action: @escaping () -> Void) {
|
||||
self.label = label
|
||||
self.complete = complete
|
||||
self.action = action
|
||||
self.width = width
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 6) {
|
||||
if complete {
|
||||
Text(.setupStepCompleteButton)
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
} else {
|
||||
Text(label)
|
||||
}
|
||||
}
|
||||
.frame(width: width)
|
||||
.padding(.vertical, 2)
|
||||
.onGeometryChange(for: CGFloat.self) { proxy in
|
||||
proxy.size.width
|
||||
} action: { newValue in
|
||||
currentWidth = newValue
|
||||
}
|
||||
}
|
||||
.preference(key: WidthKey.self, value: currentWidth)
|
||||
.primaryButton()
|
||||
.disabled(complete)
|
||||
.tint(complete ? .green : nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct StepView<Content: View>: View {
|
||||
|
||||
let title: LocalizedStringResource
|
||||
let icon: Image
|
||||
let description: LocalizedStringResource
|
||||
let actions: Content
|
||||
|
||||
init(title: LocalizedStringResource, description: LocalizedStringResource, systemImage: String, actions: () -> Content) {
|
||||
self.title = title
|
||||
self.icon = Image(systemName: systemImage)
|
||||
self.description = description
|
||||
self.actions = actions()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 24)
|
||||
Spacer()
|
||||
.frame(width: 20)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.bold()
|
||||
Text(description)
|
||||
}
|
||||
Spacer(minLength: 20)
|
||||
actions
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SetupView {
|
||||
|
||||
enum Constants {
|
||||
static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SetupView(setupComplete: .constant(false))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -36,7 +36,7 @@ struct ContentView: View {
|
||||
toolbarItem(newItemView, id: "new")
|
||||
}
|
||||
.sheet(isPresented: $runningSetup) {
|
||||
SetupView(setupComplete: $hasRunSetup)
|
||||
SetupView(visible: $runningSetup, setupComplete: $hasRunSetup)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ extension ContentView {
|
||||
}
|
||||
|
||||
var needsSetup: Bool {
|
||||
runningSetup || !hasRunSetup
|
||||
(runningSetup || !hasRunSetup || !agentStatusChecker.running) && !agentStatusChecker.developmentBuild
|
||||
}
|
||||
|
||||
/// Item either showing a "everything's good, here's more info" or "something's wrong, re-run setup" message
|
||||
@@ -66,7 +66,7 @@ extension ContentView {
|
||||
if needsSetup {
|
||||
setupNoticeView
|
||||
} else {
|
||||
agentStatusToolbarView
|
||||
runningNoticeView
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ extension ContentView {
|
||||
.foregroundColor(.white)
|
||||
})
|
||||
.buttonStyle(ToolbarButtonStyle(color: color))
|
||||
.sheet(item: $selectedUpdate) { update in
|
||||
.popover(item: $selectedUpdate, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { update in
|
||||
UpdateDetailView(update: update)
|
||||
}
|
||||
}
|
||||
@@ -103,17 +103,18 @@ extension ContentView {
|
||||
@ViewBuilder
|
||||
var newItemView: some View {
|
||||
if storeList.modifiableStore?.isAvailable ?? false {
|
||||
Button(.appMenuNewSecretButton, systemImage: "plus") {
|
||||
Button(action: {
|
||||
showingCreation = true
|
||||
}
|
||||
.menuButton()
|
||||
}, label: {
|
||||
Image(systemName: "plus")
|
||||
})
|
||||
.sheet(isPresented: $showingCreation) {
|
||||
if let modifiable = storeList.modifiableStore {
|
||||
CreateSecretView(store: modifiable) { created in
|
||||
if let created {
|
||||
activeSecret = created
|
||||
CreateSecretView(store: modifiable, showing: $showingCreation)
|
||||
.onDisappear {
|
||||
guard let newest = modifiable.secrets.last else { return }
|
||||
activeSecret = newest
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,44 +125,43 @@ extension ContentView {
|
||||
Button(action: {
|
||||
runningSetup = true
|
||||
}, label: {
|
||||
if !hasRunSetup {
|
||||
Text(.agentSetupNoticeTitle)
|
||||
.font(.headline)
|
||||
Group {
|
||||
if hasRunSetup && !agentStatusChecker.running {
|
||||
Text(.agentNotRunningNoticeTitle)
|
||||
} else {
|
||||
Text(.agentSetupNoticeTitle)
|
||||
}
|
||||
}
|
||||
.font(.headline)
|
||||
|
||||
})
|
||||
.buttonStyle(ToolbarButtonStyle(color: .orange))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var agentStatusToolbarView: some View {
|
||||
var runningNoticeView: some View {
|
||||
Button(action: {
|
||||
showingAgentInfo = true
|
||||
}, label: {
|
||||
HStack {
|
||||
if agentStatusChecker.running {
|
||||
Text(.agentRunningNoticeTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
|
||||
Circle()
|
||||
.frame(width: 10, height: 10)
|
||||
.foregroundColor(Color.green)
|
||||
} else {
|
||||
Text(.agentNotRunningNoticeTitle)
|
||||
.font(.headline)
|
||||
Circle()
|
||||
.frame(width: 10, height: 10)
|
||||
.foregroundColor(Color.red)
|
||||
}
|
||||
Text(.agentRunningNoticeTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
|
||||
Circle()
|
||||
.frame(width: 10, height: 10)
|
||||
.foregroundColor(Color.green)
|
||||
}
|
||||
})
|
||||
.buttonStyle(
|
||||
ToolbarButtonStyle(
|
||||
lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
|
||||
darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5),
|
||||
)
|
||||
)
|
||||
.buttonStyle(ToolbarButtonStyle(lightColor: .black.opacity(0.05), darkColor: .white.opacity(0.05)))
|
||||
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
||||
AgentStatusView()
|
||||
VStack {
|
||||
Text(.agentRunningNoticeDetailTitle)
|
||||
.font(.title)
|
||||
.padding(5)
|
||||
Text(.agentRunningNoticeDetailDescription)
|
||||
.frame(width: 300)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,22 +193,31 @@ extension ContentView {
|
||||
}
|
||||
|
||||
var attachmentAnchor: PopoverAttachmentAnchor {
|
||||
// Ideally .point(.bottom), but broken on Sonoma (FB12726503)
|
||||
.rect(.bounds)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#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())
|
||||
//}
|
||||
@@ -76,10 +76,10 @@ struct CopyableView: View {
|
||||
switch interactionState {
|
||||
case .hovering:
|
||||
Image(systemName: "document.on.document")
|
||||
.accessibilityLabel(String(localized: .copyableClickToCopyButton))
|
||||
.accessibilityLabel(String(localized: "copyable_click_to_copy_button"))
|
||||
case .clicking:
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.accessibilityLabel(String(localized: .copyableCopied))
|
||||
.accessibilityLabel(String(localized: "copyable_copied"))
|
||||
case .normal, .dragging:
|
||||
EmptyView()
|
||||
}
|
||||
@@ -163,12 +163,17 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
|
||||
.padding()
|
||||
#if DEBUG
|
||||
|
||||
struct CopyableView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Hello world.")
|
||||
.padding()
|
||||
CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
|
||||
.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
|
||||
@@ -4,15 +4,13 @@ import SecretKit
|
||||
struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
|
||||
@State var store: StoreType
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
var createdSecret: (AnySecret?) -> Void
|
||||
@Binding var showing: Bool
|
||||
|
||||
@State private var name = ""
|
||||
@State private var keyAttribution = ""
|
||||
@State private var authenticationRequirement: AuthenticationRequirement = .presenceRequired
|
||||
@State private var keyType: KeyType?
|
||||
@State var advanced = false
|
||||
@State var errorText: String?
|
||||
|
||||
private var authenticationOptions: [AuthenticationRequirement] {
|
||||
if advanced || authenticationRequirement == .biometryCurrent {
|
||||
@@ -96,24 +94,16 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
if let errorText {
|
||||
Section {
|
||||
} footer: {
|
||||
Text(verbatim: errorText)
|
||||
.errorStyle()
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Toggle(.createSecretAdvancedLabel, isOn: $advanced)
|
||||
.toggleStyle(.button)
|
||||
Spacer()
|
||||
Button(.createSecretCancelButton, role: .cancel) {
|
||||
dismiss()
|
||||
showing = false
|
||||
}
|
||||
Button(.createSecretCreateButton, action: save)
|
||||
.keyboardShortcut(.return)
|
||||
.primaryButton()
|
||||
.primary()
|
||||
.disabled(name.isEmpty)
|
||||
}
|
||||
.padding()
|
||||
@@ -127,25 +117,20 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
func save() {
|
||||
let attribution = keyAttribution.isEmpty ? nil : keyAttribution
|
||||
Task {
|
||||
do {
|
||||
let new = try await store.create(
|
||||
name: name,
|
||||
attributes: .init(
|
||||
keyType: keyType!,
|
||||
authentication: authenticationRequirement,
|
||||
publicKeyAttribution: attribution
|
||||
)
|
||||
try! await store.create(
|
||||
name: name,
|
||||
attributes: .init(
|
||||
keyType: keyType!,
|
||||
authentication: authenticationRequirement,
|
||||
publicKeyAttribution: attribution
|
||||
)
|
||||
createdSecret(AnySecret(new))
|
||||
dismiss()
|
||||
} catch {
|
||||
errorText = error.localizedDescription
|
||||
}
|
||||
)
|
||||
showing = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// CreateSecretView(store: Preview.StoreModifiable()) { _ in }
|
||||
//}
|
||||
#Preview {
|
||||
CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true))
|
||||
}
|
||||
@@ -28,7 +28,8 @@ struct DeleteSecretConfirmationModifier: ViewModifier {
|
||||
TextField(secret.name, text: $confirmedSecretName)
|
||||
if let errorText {
|
||||
Text(verbatim: errorText)
|
||||
.errorStyle()
|
||||
.foregroundStyle(.red)
|
||||
.font(.callout)
|
||||
}
|
||||
Button(.deleteConfirmationDeleteButton, action: delete)
|
||||
.disabled(confirmedSecretName != secret.name)
|
||||
@@ -30,22 +30,21 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} footer: {
|
||||
if let errorText {
|
||||
Text(verbatim: errorText)
|
||||
.errorStyle()
|
||||
}
|
||||
}
|
||||
if let errorText {
|
||||
Text(verbatim: errorText)
|
||||
.foregroundStyle(.red)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Button(.editSaveButton, action: rename)
|
||||
.disabled(name.isEmpty)
|
||||
.keyboardShortcut(.return)
|
||||
Button(.editCancelButton) {
|
||||
dismissalBlock(false)
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
Button(.editSaveButton, action: rename)
|
||||
.disabled(name.isEmpty)
|
||||
.keyboardShortcut(.return)
|
||||
.primaryButton()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
@@ -57,10 +57,15 @@ struct EmptyStoreModifiableView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
#Preview {
|
||||
EmptyStoreImmutableView()
|
||||
}
|
||||
#Preview {
|
||||
EmptyStoreModifiableView()
|
||||
struct EmptyStoreModifiableView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
EmptyStoreImmutableView()
|
||||
EmptyStoreModifiableView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -13,7 +13,12 @@ struct NoStoresView: View {
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NoStoresView()
|
||||
#if DEBUG
|
||||
|
||||
struct NoStoresView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NoStoresView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -6,8 +6,8 @@ struct SecretDetailView<SecretType: Secret>: View {
|
||||
let secret: SecretType
|
||||
|
||||
private let keyWriter = OpenSSHPublicKeyWriter()
|
||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL)
|
||||
|
||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID))
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
Form {
|
||||
@@ -37,6 +37,12 @@ struct SecretDetailView<SecretType: Secret>: View {
|
||||
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret"))
|
||||
//}
|
||||
#if DEBUG
|
||||
|
||||
struct SecretDetailView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SecretDetailView(secret: Preview.Store(numberOfRandomSecrets: 1).secrets[0])
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
297
Sources/Secretive/Views/SetupView.swift
Normal file
297
Sources/Secretive/Views/SetupView.swift
Normal file
@@ -0,0 +1,297 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SetupView: View {
|
||||
|
||||
@State var stepIndex = 0
|
||||
@Binding var visible: Bool
|
||||
@Binding var setupComplete: Bool
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { proxy in
|
||||
VStack {
|
||||
StepView(numberOfSteps: 3, currentStep: stepIndex, width: proxy.size.width)
|
||||
GeometryReader { _ in
|
||||
HStack(spacing: 0) {
|
||||
SecretAgentSetupView(buttonAction: advance)
|
||||
.frame(width: proxy.size.width)
|
||||
SSHAgentSetupView(buttonAction: advance)
|
||||
.frame(width: proxy.size.width)
|
||||
UpdaterExplainerView {
|
||||
visible = false
|
||||
setupComplete = true
|
||||
}
|
||||
.frame(width: proxy.size.width)
|
||||
}
|
||||
.offset(x: -proxy.size.width * Double(stepIndex), y: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 500, idealWidth: 500, minHeight: 500, idealHeight: 500)
|
||||
}
|
||||
|
||||
|
||||
func advance() {
|
||||
withAnimation(.spring()) {
|
||||
stepIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct StepView: View {
|
||||
|
||||
let numberOfSteps: Int
|
||||
let currentStep: Int
|
||||
|
||||
// Ideally we'd have a geometry reader inside this view doing this for us, but that crashes on 11.0b7
|
||||
let width: Double
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
Rectangle()
|
||||
.foregroundColor(.blue)
|
||||
.frame(height: 5)
|
||||
Rectangle()
|
||||
.foregroundColor(.green)
|
||||
.frame(width: max(0, ((width - (Constants.padding * 2)) / Double(numberOfSteps - 1)) * Double(currentStep) - (Constants.circleWidth / 2)), height: 5)
|
||||
HStack {
|
||||
ForEach(Array(0..<numberOfSteps), id: \.self) { index in
|
||||
ZStack {
|
||||
if currentStep > index {
|
||||
Circle()
|
||||
.foregroundColor(.green)
|
||||
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
|
||||
Text(.setupStepCompleteSymbol)
|
||||
.foregroundColor(.white)
|
||||
.bold()
|
||||
} else {
|
||||
Circle()
|
||||
.foregroundColor(.blue)
|
||||
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
|
||||
if currentStep == index {
|
||||
Circle()
|
||||
.strokeBorder(Color.white, lineWidth: 3)
|
||||
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
|
||||
}
|
||||
Text(String(describing: index + 1))
|
||||
.foregroundColor(.white)
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
if index < numberOfSteps - 1 {
|
||||
Spacer(minLength: 30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.padding(Constants.padding)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StepView {
|
||||
|
||||
enum Constants {
|
||||
|
||||
static let padding: Double = 15
|
||||
static let circleWidth: Double = 30
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct SetupStepView<Content> : View where Content : View {
|
||||
|
||||
let title: LocalizedStringResource
|
||||
let image: Image
|
||||
let bodyText: LocalizedStringResource
|
||||
let buttonTitle: LocalizedStringResource
|
||||
let buttonAction: () -> Void
|
||||
let content: Content
|
||||
|
||||
init(title: LocalizedStringResource, image: Image, bodyText: LocalizedStringResource, buttonTitle: LocalizedStringResource, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) {
|
||||
self.title = title
|
||||
self.image = image
|
||||
self.bodyText = bodyText
|
||||
self.buttonTitle = buttonTitle
|
||||
self.buttonAction = buttonAction
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(title)
|
||||
.font(.title)
|
||||
Spacer()
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 64)
|
||||
Spacer()
|
||||
Text(bodyText)
|
||||
.multilineTextAlignment(.center)
|
||||
Spacer()
|
||||
content
|
||||
Spacer()
|
||||
Button(buttonTitle) {
|
||||
buttonAction()
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct SecretAgentSetupView: View {
|
||||
|
||||
let buttonAction: () -> Void
|
||||
|
||||
var body: some View {
|
||||
SetupStepView(title: .setupAgentTitle,
|
||||
image: Image(nsImage: NSApplication.shared.applicationIconImage),
|
||||
bodyText: .setupAgentDescription,
|
||||
buttonTitle: .setupAgentInstallButton,
|
||||
buttonAction: install) {
|
||||
Text(.setupAgentActivityMonitorDescription)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
|
||||
func install() {
|
||||
Task {
|
||||
await LaunchAgentController().install()
|
||||
buttonAction()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct SSHAgentSetupView: View {
|
||||
|
||||
let buttonAction: () -> Void
|
||||
|
||||
private static let controller = ShellConfigurationController()
|
||||
@State private var selectedShellInstruction: ShellConfigInstruction = controller.shellInstructions.first!
|
||||
|
||||
var body: some View {
|
||||
SetupStepView(title: .setupSshTitle,
|
||||
image: Image(systemName: "terminal"),
|
||||
bodyText: .setupSshDescription,
|
||||
buttonTitle: .setupSshAddedManuallyButton,
|
||||
buttonAction: buttonAction) {
|
||||
Link(.setupThirdPartyFaqLink, destination: URL(string: "https://github.com/maxgoedjen/secretive/blob/main/APP_CONFIG.md")!)
|
||||
Picker(selection: $selectedShellInstruction, label: EmptyView()) {
|
||||
ForEach(SSHAgentSetupView.controller.shellInstructions) { instruction in
|
||||
Text(instruction.shell)
|
||||
.tag(instruction)
|
||||
.padding()
|
||||
}
|
||||
}.pickerStyle(SegmentedPickerStyle())
|
||||
CopyableView(title: .setupSshAddToConfigButton(configPath: selectedShellInstruction.shellConfigPath), image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text)
|
||||
Button(.setupSshAddForMeButton) {
|
||||
let controller = ShellConfigurationController()
|
||||
if controller.addToShell(shellInstructions: selectedShellInstruction) {
|
||||
buttonAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Delegate: NSObject, NSOpenSavePanelDelegate {
|
||||
|
||||
private let name: String
|
||||
|
||||
init(name: String) {
|
||||
self.name = name
|
||||
}
|
||||
|
||||
func panel(_ sender: Any, shouldEnable url: URL) -> Bool {
|
||||
return url.lastPathComponent == name
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct UpdaterExplainerView: View {
|
||||
|
||||
let buttonAction: () -> Void
|
||||
|
||||
var body: some View {
|
||||
SetupStepView(title: .setupUpdatesTitle,
|
||||
image: Image(systemName: "dot.radiowaves.left.and.right"),
|
||||
bodyText: .setupUpdatesDescription,
|
||||
buttonTitle: .setupUpdatesOk,
|
||||
buttonAction: buttonAction) {
|
||||
Link(.setupUpdatesReadmore, destination: SetupView.Constants.updaterFAQURL)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SetupView {
|
||||
|
||||
enum Constants {
|
||||
static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ShellConfigInstruction: Identifiable, Hashable {
|
||||
|
||||
var shell: String
|
||||
var shellConfigDirectory: String
|
||||
var shellConfigFilename: String
|
||||
var text: String
|
||||
|
||||
var id: String {
|
||||
shell
|
||||
}
|
||||
|
||||
var shellConfigPath: String {
|
||||
return (shellConfigDirectory as NSString).appendingPathComponent(shellConfigFilename)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct SetupView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
SetupView(visible: .constant(true), setupComplete: .constant(false))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct SecretAgentSetupView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
SecretAgentSetupView(buttonAction: {})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct SSHAgentSetupView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
SSHAgentSetupView(buttonAction: {})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct UpdaterExplainerView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
UpdaterExplainerView(buttonAction: {})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,94 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct PrimaryButtonModifier: ViewModifier {
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.isEnabled) var isEnabled
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
// Tinted glass prominent is really hard to read on 26.0.
|
||||
if #available(macOS 26.0, *), colorScheme == .dark, isEnabled {
|
||||
content.buttonStyle(.glassProminent)
|
||||
} else {
|
||||
content.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension View {
|
||||
|
||||
func primaryButton() -> some View {
|
||||
modifier(PrimaryButtonModifier())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct MenuButtonModifier: ViewModifier {
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(macOS 26.0, *) {
|
||||
content
|
||||
.glassEffect(.regular.tint(.white.opacity(0.1)), in: .circle)
|
||||
} else {
|
||||
content
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension View {
|
||||
|
||||
func menuButton() -> some View {
|
||||
modifier(MenuButtonModifier())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct NormalButtonModifier: ViewModifier {
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(macOS 26.0, *) {
|
||||
content.buttonStyle(.glass)
|
||||
} else {
|
||||
content.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension View {
|
||||
|
||||
func normalButton() -> some View {
|
||||
modifier(NormalButtonModifier())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct DangerButtonModifier: ViewModifier {
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
// Tinted glass prominent is really hard to read on 26.0.
|
||||
if #available(macOS 26.0, *), colorScheme == .dark {
|
||||
content.buttonStyle(.glassProminent)
|
||||
.tint(.red)
|
||||
.foregroundStyle(.white)
|
||||
} else {
|
||||
content.buttonStyle(.borderedProminent)
|
||||
.tint(.red)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension View {
|
||||
|
||||
func danger() -> some View {
|
||||
modifier(DangerButtonModifier())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ErrorStyleModifier: ViewModifier {
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.foregroundStyle(.red)
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension View {
|
||||
|
||||
func errorStyle() -> some View {
|
||||
modifier(ErrorStyleModifier())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AgentStatusView: View {
|
||||
|
||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||
|
||||
var body: some View {
|
||||
if agentStatusChecker.running {
|
||||
AgentRunningView()
|
||||
} else {
|
||||
AgentNotRunningView()
|
||||
}
|
||||
}
|
||||
}
|
||||
struct AgentRunningView: View {
|
||||
|
||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
if let process = agentStatusChecker.process {
|
||||
ConfigurationItemView(
|
||||
title: .agentDetailsLocationTitle,
|
||||
value: process.bundleURL!.path(),
|
||||
action: .revealInFinder(process.bundleURL!.path()),
|
||||
)
|
||||
ConfigurationItemView(
|
||||
title: .agentDetailsSocketPathTitle,
|
||||
value: URL.socketPath,
|
||||
action: .copy(URL.socketPath),
|
||||
)
|
||||
ConfigurationItemView(
|
||||
title: .agentDetailsVersionTitle,
|
||||
value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
|
||||
)
|
||||
if let launchDate = process.launchDate {
|
||||
ConfigurationItemView(
|
||||
title: .agentDetailsRunningSinceTitle,
|
||||
value: launchDate.formatted()
|
||||
)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text(.agentRunningNoticeDetailTitle)
|
||||
.font(.headline)
|
||||
.padding(.top)
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(.agentRunningNoticeDetailDescription)
|
||||
HStack {
|
||||
Spacer()
|
||||
Menu(.agentDetailsRestartAgentButton) {
|
||||
Button(.agentDetailsDisableAgentButton) {
|
||||
Task {
|
||||
_ = await LaunchAgentController()
|
||||
.uninstall()
|
||||
agentStatusChecker.check()
|
||||
}
|
||||
}
|
||||
} primaryAction: {
|
||||
Task {
|
||||
let controller = LaunchAgentController()
|
||||
let installed = await controller.install()
|
||||
if !installed {
|
||||
_ = await controller.forceLaunch()
|
||||
}
|
||||
agentStatusChecker.check()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical)
|
||||
}
|
||||
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.frame(width: 400)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct AgentNotRunningView: View {
|
||||
|
||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||
@State var triedRestart = false
|
||||
@State var loading = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
} header: {
|
||||
Text(.agentNotRunningNoticeTitle)
|
||||
.font(.headline)
|
||||
.padding(.top)
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(.agentNotRunningNoticeDetailDescription)
|
||||
HStack {
|
||||
if !triedRestart {
|
||||
Spacer()
|
||||
Button {
|
||||
guard !loading else { return }
|
||||
loading = true
|
||||
Task {
|
||||
let controller = LaunchAgentController()
|
||||
let installed = await controller.install()
|
||||
if !installed {
|
||||
_ = await controller.forceLaunch()
|
||||
}
|
||||
agentStatusChecker.check()
|
||||
loading = false
|
||||
|
||||
if !agentStatusChecker.running {
|
||||
triedRestart = true
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
if !loading {
|
||||
Text(.agentDetailsStartAgentButton)
|
||||
} else {
|
||||
HStack {
|
||||
Text(.agentDetailsStartAgentButtonStarting)
|
||||
ProgressView()
|
||||
.controlSize(.mini)
|
||||
}
|
||||
}
|
||||
}
|
||||
.primaryButton()
|
||||
} else {
|
||||
Text(.agentDetailsCouldNotStartError)
|
||||
.bold()
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.frame(width: 400)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// AgentStatusView()
|
||||
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
|
||||
//}
|
||||
//#Preview {
|
||||
// AgentStatusView()
|
||||
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current))
|
||||
//}
|
||||
Reference in New Issue
Block a user