This commit is contained in:
Max Goedjen 2025-08-30 18:56:52 -07:00
parent 19760f1e02
commit b949d846c1
No known key found for this signature in database
11 changed files with 339 additions and 399 deletions

View File

@ -1,11 +1,19 @@
{ {
"sourceLanguage" : "en", "sourceLanguage" : "en",
"strings" : { "strings" : {
".zshrc" : {
},
"Add Automatically" : { "Add Automatically" : {
},
"agent_not_running_notice_detail_description" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "SecretAgent is a process that runs in the background to sign requests, so you don't need to keep Secretive open all the time.\n\n**Secretive will not be able to function properly unless the agent is installed and running.**"
}
}
}
}, },
"agent_not_running_notice_title" : { "agent_not_running_notice_title" : {
"extractionState" : "manual", "extractionState" : "manual",
@ -1169,6 +1177,9 @@
}, },
"Configure" : { "Configure" : {
},
"Copy" : {
}, },
"copyable_click_to_copy_button" : { "copyable_click_to_copy_button" : {
"extractionState" : "manual", "extractionState" : "manual",
@ -2489,6 +2500,9 @@
} }
} }
} }
},
"Disable Agent" : {
}, },
"Done" : { "Done" : {
@ -3406,6 +3420,18 @@
} }
} }
} }
},
"Restart Agent" : {
},
"Reveal in Finder" : {
},
"Running Since" : {
},
"Secret Agent Location" : {
}, },
"secret_detail_md5_fingerprint_label" : { "secret_detail_md5_fingerprint_label" : {
"extractionState" : "manual", "extractionState" : "manual",
@ -4335,9 +4361,6 @@
} }
} }
} }
},
"setup_ssh_add_to_config_button_%@" : {
}, },
"setup_ssh_added_manually_button" : { "setup_ssh_added_manually_button" : {
"extractionState" : "manual", "extractionState" : "manual",
@ -5205,7 +5228,10 @@
} }
} }
}, },
"TfileDialogMessageest" : { "Socket Path" : {
},
"Start Agent" : {
}, },
"unnamed_secret" : { "unnamed_secret" : {
@ -6153,6 +6179,9 @@
} }
} }
} }
},
"Version" : {
} }
}, },
"version" : "1.0" "version" : "1.0"

View File

@ -36,7 +36,6 @@
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; }; 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; }; 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; }; 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; };
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; }; 506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; }; 506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; }; 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; };
@ -52,6 +51,7 @@
50AE97002E5C1A420018C710 /* ConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */; }; 50AE97002E5C1A420018C710 /* ConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */; };
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; }; 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; };
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; }; 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; };
50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */; };
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; }; 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; }; 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -121,7 +121,6 @@
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = "<group>"; };
@ -142,6 +141,7 @@
50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationView.swift; sourceTree = "<group>"; }; 50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationView.swift; sourceTree = "<group>"; };
50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.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>"; }; 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>"; };
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.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>"; }; 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -256,6 +256,7 @@
506772C82425BB8500034DED /* NoStoresView.swift */, 506772C82425BB8500034DED /* NoStoresView.swift */,
50153E1F250AFCB200525160 /* UpdateView.swift */, 50153E1F250AFCB200525160 /* UpdateView.swift */,
5066A6C12516F303004B5A36 /* SetupView.swift */, 5066A6C12516F303004B5A36 /* SetupView.swift */,
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */, 50AE96FF2E5C1A420018C710 /* ConfigurationView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */, 5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
); );
@ -269,7 +270,6 @@
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */, 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
50571E0424393D1500F76F6C /* LaunchAgentController.swift */, 50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */,
); );
path = Controllers; path = Controllers;
sourceTree = "<group>"; sourceTree = "<group>";
@ -445,8 +445,8 @@
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */, 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */,
50033AC327813F1700253856 /* BundleIDs.swift in Sources */, 50033AC327813F1700253856 /* BundleIDs.swift in Sources */,
50BDCB722E63BAF20072D2E7 /* AgentStatusView.swift in Sources */,
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */, 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,

View File

@ -87,7 +87,7 @@ extension Secretive {
private func reinstallAgent() { private func reinstallAgent() {
justUpdatedChecker.check() justUpdatedChecker.check()
Task { Task {
await LaunchAgentController().install() _ = await LaunchAgentController().install()
try? await Task.sleep(for: .seconds(1)) try? await Task.sleep(for: .seconds(1))
agentStatusChecker.check() agentStatusChecker.check()
if !agentStatusChecker.running { if !agentStatusChecker.running {

View File

@ -6,12 +6,14 @@ import Observation
@MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable { @MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable {
var running: Bool { get } var running: Bool { get }
var developmentBuild: Bool { get } var developmentBuild: Bool { get }
var process: NSRunningApplication? { get }
func check() func check()
} }
@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol { @Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol {
var running: Bool = false var running: Bool = false
var process: NSRunningApplication? = nil
nonisolated init() { nonisolated init() {
Task { @MainActor in Task { @MainActor in
@ -20,7 +22,8 @@ import Observation
} }
func check() { func check() {
running = instanceSecretAgentProcess != nil process = instanceSecretAgentProcess
running = process != nil
} }
// All processes, including ones from older versions, etc // All processes, including ones from older versions, etc
@ -34,7 +37,7 @@ import Observation
let agents = allSecretAgentProcesses let agents = allSecretAgentProcesses
for agent in agents { for agent in agents {
guard let url = agent.bundleURL else { continue } guard let url = agent.bundleURL else { continue }
if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) { if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) || (url.isXcodeURL && developmentBuild) {
return agent return agent
} }
} }
@ -43,9 +46,15 @@ import Observation
// Whether Secretive is being run in an Xcode environment. // Whether Secretive is being run in an Xcode environment.
var developmentBuild: Bool { var developmentBuild: Bool {
Bundle.main.bundleURL.absoluteString.contains("/Library/Developer/Xcode") Bundle.main.bundleURL.isXcodeURL
} }
} }
extension URL {
var isXcodeURL: Bool {
absoluteString.contains("/Library/Developer/Xcode")
}
}

View File

@ -8,15 +8,23 @@ struct LaunchAgentController {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController") private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
func install() async { func install() async -> Bool {
logger.debug("Installing agent") logger.debug("Installing agent")
_ = setEnabled(false) _ = setEnabled(false)
// This is definitely a bit of a "seems to work better" thing but: // 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 // Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
// and start new? // and start new?
try? await Task.sleep(for: .seconds(1)) try? await Task.sleep(for: .seconds(1))
await MainActor.run { return await MainActor.run {
_ = setEnabled(true) setEnabled(true)
}
}
func uninstall() async -> Bool {
logger.debug("Uninstalling agent")
try? await Task.sleep(for: .seconds(1))
return await MainActor.run {
setEnabled(false)
} }
} }

View File

@ -1,63 +0,0 @@
import Foundation
import Cocoa
import SecretKit
struct ShellConfigurationController {
let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.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
}
}

View File

@ -1,12 +1,15 @@
import Foundation import Foundation
import AppKit
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol { class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
let running: Bool let running: Bool
let process: NSRunningApplication?
let developmentBuild = false let developmentBuild = false
init(running: Bool = true) { init(running: Bool = true, process: NSRunningApplication? = nil) {
self.running = running self.running = running
self.process = process
} }
func check() { func check() {

View File

@ -22,3 +22,30 @@ extension View {
} }
} }
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())
}
}

View File

@ -0,0 +1,153 @@
import SwiftUI
struct AgentStatusView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
var body: some View {
if agentStatusChecker.running {
Form {
Section {
if let process = agentStatusChecker.process {
AgentInformationView(
title: "Secret Agent Location",
value: process.bundleURL!.path(),
actions: [.revealInFinder],
)
AgentInformationView(
title: "Socket Path",
value: socketPath,
actions: [.copy],
)
AgentInformationView(
title: "Version",
value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
)
if let launchDate = process.launchDate {
AgentInformationView(
title: "Running Since",
value: launchDate.formatted()
)
}
}
} header: {
Text(.agentRunningNoticeDetailTitle)
.font(.headline)
.padding(.top)
} footer: {
VStack(alignment: .leading) {
Text(.agentRunningNoticeDetailDescription)
HStack {
Spacer()
Menu("Restart Agent") {
Button("Disable Agent") {
Task {
await LaunchAgentController()
.uninstall()
}
}
} primaryAction: {
Task {
let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
}
}
}
}
.padding(.vertical)
}
}
.formStyle(.grouped)
.frame(width: 400)
} else {
Form {
Section {
} header: {
Text(.agentNotRunningNoticeTitle)
.font(.headline)
.padding(.top)
} footer: {
Text(.agentNotRunningNoticeDetailDescription)
Spacer()
HStack {
Spacer()
Button("Start Agent") {
Task {
let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
}
}
.primary()
}
.padding(.vertical)
}
}
.formStyle(.grouped)
.frame(width: 400)
}
}
}
struct AgentInformationView: View {
enum Action {
case copy
case revealInFinder
}
let title: LocalizedStringResource
let value: String
let actions: Set<Action>
@State var tapping = false
init(title: LocalizedStringResource, value: String, actions: Set<Action> = []) {
self.title = title
self.value = value
self.actions = actions
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(title)
Spacer()
if actions.contains(.revealInFinder) {
Button("Reveal in Finder", systemImage: "folder") {
NSWorkspace.shared.selectFile(value, inFileViewerRootedAtPath: value)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
}
if actions.contains(.copy) {
Button("Copy", systemImage: "document.on.document") {
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
}
}
Text(value)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
#Preview {
AgentStatusView()
.environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
}
#Preview {
AgentStatusView()
.environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current))
}

View File

@ -56,7 +56,7 @@ extension ContentView {
} }
var needsSetup: Bool { var needsSetup: Bool {
(runningSetup || !hasRunSetup || !agentStatusChecker.running) && !agentStatusChecker.developmentBuild runningSetup || !hasRunSetup
} }
/// Item either showing a "everything's good, here's more info" or "something's wrong, re-run setup" message /// 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 { if needsSetup {
setupNoticeView setupNoticeView
} else { } else {
runningNoticeView agentStatusToolbarView
} }
} }
@ -125,43 +125,44 @@ extension ContentView {
Button(action: { Button(action: {
runningSetup = true runningSetup = true
}, label: { }, label: {
Group { if !hasRunSetup {
if hasRunSetup && !agentStatusChecker.running { Text(.agentSetupNoticeTitle)
Text(.agentNotRunningNoticeTitle) .font(.headline)
} else {
Text(.agentSetupNoticeTitle)
}
} }
.font(.headline)
}) })
.buttonStyle(ToolbarButtonStyle(color: .orange)) .buttonStyle(ToolbarButtonStyle(color: .orange))
} }
@ViewBuilder @ViewBuilder
var runningNoticeView: some View { var agentStatusToolbarView: some View {
Button(action: { Button(action: {
showingAgentInfo = true showingAgentInfo = true
}, label: { }, label: {
HStack { HStack {
Text(.agentRunningNoticeTitle) if agentStatusChecker.running {
.font(.headline) Text(.agentRunningNoticeTitle)
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white) .font(.headline)
Circle() .foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
.frame(width: 10, height: 10) Circle()
.foregroundColor(Color.green) .frame(width: 10, height: 10)
.foregroundColor(Color.green)
} else {
Text(.agentNotRunningNoticeTitle)
.font(.headline)
Circle()
.frame(width: 10, height: 10)
.foregroundColor(Color.red)
}
} }
}) })
.buttonStyle(ToolbarButtonStyle(lightColor: .black.opacity(0.05), darkColor: .white.opacity(0.05))) .buttonStyle(
ToolbarButtonStyle(
lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5),
)
)
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { .popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
VStack { AgentStatusView()
Text(.agentRunningNoticeDetailTitle)
.font(.title)
.padding(5)
Text(.agentRunningNoticeDetailDescription)
.frame(width: 300)
}
.padding()
} }
} }
@ -193,7 +194,6 @@ extension ContentView {
} }
var attachmentAnchor: PopoverAttachmentAnchor { var attachmentAnchor: PopoverAttachmentAnchor {
// Ideally .point(.bottom), but broken on Sonoma (FB12726503)
.rect(.bounds) .rect(.bounds)
} }

View File

@ -19,8 +19,7 @@ struct SetupView: View {
) { ) {
OnboardingButton("setup_agent_install_button", installed) { OnboardingButton("setup_agent_install_button", installed) {
Task { Task {
await LaunchAgentController().install() installed = await LaunchAgentController().install()
installed = true
} }
} }
} }
@ -30,7 +29,7 @@ struct SetupView: View {
description: "setup_updates_description", description: "setup_updates_description",
systemImage: "network.badge.shield.half.filled", systemImage: "network.badge.shield.half.filled",
) { ) {
OnboardingButton("setup_updates_ok", false) { OnboardingButton("setup_updates_ok", updates) {
Task { Task {
updates = true updates = true
} }
@ -43,27 +42,9 @@ struct SetupView: View {
systemImage: "network.badge.shield.half.filled", systemImage: "network.badge.shield.half.filled",
) { ) {
HStack { HStack {
OnboardingButton("setup_ssh_added_manually_button", false) { OnboardingButton("Configure", false) {
sshConfig = true // sshConfig = true
} }
OnboardingButton("Add Automatically", false) {
// let controller = ShellConfigurationController()
// if controller.addToShell(shellInstructions: selectedShellInstruction) {
// }
sshConfig = true
}
.fileImporter(isPresented: $sshConfig, allowedContentTypes: [.utf8PlainText, .symbolicLink, .data]) { result in
print(result)
}
// FIXME:
.fileDialogDefaultDirectory(URL(fileURLWithPath: "/Users/max/"))
.fileDialogBrowserOptions([.displayFileExtensions, .includeHiddenFiles])
.fileExporterFilenameLabel(Text(".zshrc"))
.fileDialogMessage("TfileDialogMessageest")
.fileDialogConfirmationLabel("Configure")
.fileDialogURLEnabled(#Predicate {
return $0.lastPathComponent == ".zshrc" || $0.hasDirectoryPath
})
} }
} }
} }
@ -152,232 +133,61 @@ struct NewStepView<Content: View>: View {
} }
struct OldSetupView: View { //struct SSHAgentSetupView: View {
//
@State var stepIndex = 0 // let buttonAction: () -> Void
@Binding var visible: Bool //
@Binding var setupComplete: Bool // @State private var selectedShellInstruction: ShellConfigInstruction?
//
var body: some View { // private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
GeometryReader { proxy in //
VStack { // private var shellInstructions: [ShellConfigInstruction] {
StepView(numberOfSteps: 3, currentStep: stepIndex, width: proxy.size.width) // [
GeometryReader { _ in // ShellConfigInstruction(shell: "global",
HStack(spacing: 0) { // shellConfigDirectory: "~/.ssh/",
SecretAgentSetupView(buttonAction: advance) // shellConfigFilename: "config",
.frame(width: proxy.size.width) // text: "Host *\n\tIdentityAgent \(socketPath)"),
SSHAgentSetupView(buttonAction: advance) // ShellConfigInstruction(shell: "zsh",
.frame(width: proxy.size.width) // shellConfigDirectory: "~/",
UpdaterExplainerView { // shellConfigFilename: ".zshrc",
visible = false // text: "export SSH_AUTH_SOCK=\(socketPath)"),
setupComplete = true // ShellConfigInstruction(shell: "bash",
} // shellConfigDirectory: "~/",
.frame(width: proxy.size.width) // shellConfigFilename: ".bashrc",
} // text: "export SSH_AUTH_SOCK=\(socketPath)"),
.offset(x: -proxy.size.width * Double(stepIndex), y: 0) // ShellConfigInstruction(shell: "fish",
} // shellConfigDirectory: "~/.config/fish",
} // shellConfigFilename: "config.fish",
} // text: "set -x SSH_AUTH_SOCK \(socketPath)"),
.frame(minWidth: 500, idealWidth: 500, minHeight: 500, idealHeight: 500) // ]
} //
// }
//
func advance() { // var body: some View {
withAnimation(.spring()) { // SetupStepView(title: "setup_ssh_title",
stepIndex += 1 // image: Image(systemName: "terminal"),
} // bodyText: "setup_ssh_description",
} // buttonTitle: "setup_ssh_added_manually_button",
// buttonAction: buttonAction) {
} // Link("setup_third_party_faq_link", destination: URL(string: "https://github.com/maxgoedjen/secretive/blob/main/APP_CONFIG.md")!)
// Picker(selection: $selectedShellInstruction, label: EmptyView()) {
struct StepView: View { // ForEach(SSHAgentSetupView.controller.shellInstructions) { instruction in
// Text(instruction.shell)
let numberOfSteps: Int // .tag(instruction)
let currentStep: Int // .padding()
// }
// Ideally we'd have a geometry reader inside this view doing this for us, but that crashes on 11.0b7 // }.pickerStyle(SegmentedPickerStyle())
let width: Double // CopyableView(title: "setup_ssh_add_to_config_button_\(selectedShellInstruction.shellConfigPath)", image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text)
// Button("setup_ssh_add_for_me_button") {
var body: some View { // let controller = ShellConfigurationController()
ZStack(alignment: .leading) { // if controller.addToShell(shellInstructions: selectedShellInstruction) {
Rectangle() // buttonAction()
.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("setup_step_complete_symbol")
.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: LocalizedStringKey
let image: Image
let bodyText: LocalizedStringKey
let buttonTitle: LocalizedStringKey
let buttonAction: () -> Void
let content: Content
init(title: LocalizedStringKey, image: Image, bodyText: LocalizedStringKey, buttonTitle: LocalizedStringKey, 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: "setup_agent_title",
image: Image(nsImage: NSApplication.shared.applicationIconImage),
bodyText: "setup_agent_description",
buttonTitle: "setup_agent_install_button",
buttonAction: install) {
Text("setup_agent_activity_monitor_description")
.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: "setup_ssh_title",
image: Image(systemName: "terminal"),
bodyText: "setup_ssh_description",
buttonTitle: "setup_ssh_added_manually_button",
buttonAction: buttonAction) {
Link("setup_third_party_faq_link", 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: "setup_ssh_add_to_config_button_\(selectedShellInstruction.shellConfigPath)", image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text)
Button("setup_ssh_add_for_me_button") {
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: "setup_updates_title",
image: Image(systemName: "dot.radiowaves.left.and.right"),
bodyText: "setup_updates_description",
buttonTitle: "setup_updates_ok",
buttonAction: buttonAction) {
Link("setup_updates_readmore", destination: SetupView.Constants.updaterFAQURL)
}
}
}
extension SetupView { extension SetupView {
@ -404,46 +214,10 @@ struct ShellConfigInstruction: Identifiable, Hashable {
} }
#if DEBUG #Preview {
SetupView(visible: .constant(true), setupComplete: .constant(false))
struct SetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
SetupView(visible: .constant(true), setupComplete: .constant(false))
}
}
} }
struct SecretAgentSetupView_Previews: PreviewProvider { //#Preview {
// SSHAgentSetupView(buttonAction: {})
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