import Foundation import SwiftUI struct SetupView: View { var completion: ((Bool) -> Void)? var body: some View { Form { SetupStepView(text: "Secretive needs to install a helper app to sign requests when the main app isn't running. This app is called \"SecretAgent\" and you might see it in Activity Manager from time to time.", index: 1, nestedView: nil, actionText: "Install") { installLaunchAgent() } SetupStepView(text: "Add this line to your shell config (.bashrc or .zshrc) telling SSH to talk to SecretAgent when it wants to authenticate. Drag this into your config file.", index: 2, nestedView: SetupStepCommandView(instructions: Constants.socketPrompts, selectedShellInstruction: Constants.socketPrompts.first!), actionText: "Added") { markAsDone() } SetupStepView(text: "Secretive will periodically check with GitHub to see if there's a new release. If you see any network requests to GitHub, that's why.", index: 3, nestedView: nil, actionText: "Got it") { markAsDone() } HStack { Spacer() Button(action: { completion?(true) }) { Text("Finish") } .padding() } }.frame(minWidth: 640, minHeight: 400) } } struct SetupStepView: View { let text: String let index: Int let nestedView: NestedViewType? @State var completed = false let actionText: String let action: (() -> Bool) var body: some View { Section { HStack { ZStack { if completed { Circle().foregroundColor(.green) .frame(width: 30, height: 30) Text("✓") .foregroundColor(.white) .bold() } else { Circle().foregroundColor(.blue) .frame(width: 30, height: 30) Text(String(describing: index)) .foregroundColor(.white) .bold() } } .padding() VStack { Text(text) .opacity(completed ? 0.5 : 1) .lineLimit(nil) if nestedView != nil { nestedView!.padding() } } .padding() Button(action: { completed = action() }) { Text(actionText) }.disabled(completed) .padding() } } } } struct SetupStepCommandView: View { let instructions: [ShellConfigInstruction] @State var selectedShellInstruction: ShellConfigInstruction var body: some View { TabView(selection: $selectedShellInstruction) { ForEach(instructions) { instruction in VStack(alignment: .leading) { Text(instruction.text) .lineLimit(nil) .font(.system(.caption, design: .monospaced)) .multilineTextAlignment(.leading) .frame(minHeight: 50) HStack { Spacer() Button(action: copy) { Text("Copy") } } }.tabItem { Text(instruction.shell) } .tag(instruction) .padding() } } .onDrag { return NSItemProvider(item: NSData(data: selectedShellInstruction.text.data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String) } } func copy() { NSPasteboard.general.declareTypes([.string], owner: nil) NSPasteboard.general.setString(selectedShellInstruction.text, forType: .string) } } extension SetupView { func installLaunchAgent() -> Bool { LaunchAgentController().install() } func markAsDone() -> Bool { return true } } extension SetupView { enum Constants { static let socketPath = (NSHomeDirectory().replacingOccurrences(of: "com.maxgoedjen.Secretive.Host", with: "com.maxgoedjen.Secretive.SecretAgent") as NSString).appendingPathComponent("socket.ssh") as String static let socketPrompts: [ShellConfigInstruction] = [ ShellConfigInstruction(shell: "zsh", text: "export SSH_AUTH_SOCK=\(socketPath)"), ShellConfigInstruction(shell: "bash", text: "export SSH_AUTH_SOCK=\(socketPath)"), ShellConfigInstruction(shell: "fish", text: "set -x SSH_AUTH_SOCK=\(socketPath)"), ] } } struct ShellConfigInstruction: Identifiable, Hashable { var shell: String var text: String var id: String { shell } } #if DEBUG struct SetupView_Previews: PreviewProvider { static var previews: some View { SetupView() } } #endif