mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-03-10 19:47:24 +01:00
WIP
This commit is contained in:
@@ -87,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,12 +6,14 @@ 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
|
||||
@@ -20,7 +22,8 @@ import Observation
|
||||
}
|
||||
|
||||
func check() {
|
||||
running = instanceSecretAgentProcess != nil
|
||||
process = instanceSecretAgentProcess
|
||||
running = process != nil
|
||||
}
|
||||
|
||||
// All processes, including ones from older versions, etc
|
||||
@@ -34,7 +37,7 @@ import Observation
|
||||
let agents = allSecretAgentProcesses
|
||||
for agent in agents {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -43,9 +46,15 @@ import Observation
|
||||
|
||||
// Whether Secretive is being run in an Xcode environment.
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,15 +8,23 @@ struct LaunchAgentController {
|
||||
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
|
||||
|
||||
func install() async {
|
||||
func install() async -> Bool {
|
||||
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))
|
||||
await MainActor.run {
|
||||
_ = setEnabled(true)
|
||||
return await MainActor.run {
|
||||
setEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
func uninstall() async -> Bool {
|
||||
logger.debug("Uninstalling agent")
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
return await MainActor.run {
|
||||
setEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
|
||||
|
||||
let running: Bool
|
||||
let process: NSRunningApplication?
|
||||
let developmentBuild = false
|
||||
|
||||
init(running: Bool = true) {
|
||||
init(running: Bool = true, process: NSRunningApplication? = nil) {
|
||||
self.running = running
|
||||
self.process = process
|
||||
}
|
||||
|
||||
func check() {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
153
Sources/Secretive/Views/AgentStatusView.swift
Normal file
153
Sources/Secretive/Views/AgentStatusView.swift
Normal 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))
|
||||
}
|
||||
@@ -56,7 +56,7 @@ extension ContentView {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -66,7 +66,7 @@ extension ContentView {
|
||||
if needsSetup {
|
||||
setupNoticeView
|
||||
} else {
|
||||
runningNoticeView
|
||||
agentStatusToolbarView
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,43 +125,44 @@ extension ContentView {
|
||||
Button(action: {
|
||||
runningSetup = true
|
||||
}, label: {
|
||||
Group {
|
||||
if hasRunSetup && !agentStatusChecker.running {
|
||||
Text(.agentNotRunningNoticeTitle)
|
||||
} else {
|
||||
Text(.agentSetupNoticeTitle)
|
||||
}
|
||||
if !hasRunSetup {
|
||||
Text(.agentSetupNoticeTitle)
|
||||
.font(.headline)
|
||||
}
|
||||
.font(.headline)
|
||||
|
||||
})
|
||||
.buttonStyle(ToolbarButtonStyle(color: .orange))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var runningNoticeView: some View {
|
||||
var agentStatusToolbarView: some View {
|
||||
Button(action: {
|
||||
showingAgentInfo = true
|
||||
}, label: {
|
||||
HStack {
|
||||
Text(.agentRunningNoticeTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
|
||||
Circle()
|
||||
.frame(width: 10, height: 10)
|
||||
.foregroundColor(Color.green)
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
.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) {
|
||||
VStack {
|
||||
Text(.agentRunningNoticeDetailTitle)
|
||||
.font(.title)
|
||||
.padding(5)
|
||||
Text(.agentRunningNoticeDetailDescription)
|
||||
.frame(width: 300)
|
||||
}
|
||||
.padding()
|
||||
AgentStatusView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +194,6 @@ extension ContentView {
|
||||
}
|
||||
|
||||
var attachmentAnchor: PopoverAttachmentAnchor {
|
||||
// Ideally .point(.bottom), but broken on Sonoma (FB12726503)
|
||||
.rect(.bounds)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@ struct SetupView: View {
|
||||
) {
|
||||
OnboardingButton("setup_agent_install_button", installed) {
|
||||
Task {
|
||||
await LaunchAgentController().install()
|
||||
installed = true
|
||||
installed = await LaunchAgentController().install()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +29,7 @@ struct SetupView: View {
|
||||
description: "setup_updates_description",
|
||||
systemImage: "network.badge.shield.half.filled",
|
||||
) {
|
||||
OnboardingButton("setup_updates_ok", false) {
|
||||
OnboardingButton("setup_updates_ok", updates) {
|
||||
Task {
|
||||
updates = true
|
||||
}
|
||||
@@ -43,27 +42,9 @@ struct SetupView: View {
|
||||
systemImage: "network.badge.shield.half.filled",
|
||||
) {
|
||||
HStack {
|
||||
OnboardingButton("setup_ssh_added_manually_button", false) {
|
||||
sshConfig = true
|
||||
OnboardingButton("Configure", false) {
|
||||
// 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 {
|
||||
|
||||
@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("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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
//struct SSHAgentSetupView: View {
|
||||
//
|
||||
// let buttonAction: () -> Void
|
||||
//
|
||||
// @State private var selectedShellInstruction: ShellConfigInstruction?
|
||||
//
|
||||
// private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
|
||||
//
|
||||
// private 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)"),
|
||||
// ]
|
||||
//
|
||||
// }
|
||||
//
|
||||
// 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()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
extension SetupView {
|
||||
|
||||
@@ -404,46 +214,10 @@ struct ShellConfigInstruction: Identifiable, Hashable {
|
||||
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct SetupView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
SetupView(visible: .constant(true), setupComplete: .constant(false))
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
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
|
||||
//#Preview {
|
||||
// SSHAgentSetupView(buttonAction: {})
|
||||
//}
|
||||
|
||||
Reference in New Issue
Block a user