mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-09-20 03:10:57 +00:00
.
This commit is contained in:
parent
b21abbb49f
commit
9ca859fa3f
@ -27,6 +27,9 @@
|
||||
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 */; };
|
||||
@ -112,6 +115,9 @@
|
||||
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; };
|
||||
@ -187,6 +193,55 @@
|
||||
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 = (
|
||||
@ -249,24 +304,10 @@
|
||||
508A58B0241ED1C40069DC07 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50617D8423FCE48E0099B055 /* ContentView.swift */,
|
||||
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
|
||||
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
|
||||
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
|
||||
50153E21250DECA300525160 /* SecretListItemView.swift */,
|
||||
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
|
||||
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
|
||||
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
|
||||
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */,
|
||||
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
|
||||
506772C82425BB8500034DED /* NoStoresView.swift */,
|
||||
50153E1F250AFCB200525160 /* UpdateView.swift */,
|
||||
5066A6C12516F303004B5A36 /* SetupView.swift */,
|
||||
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
|
||||
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */,
|
||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
||||
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
|
||||
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */,
|
||||
504788EF2E681ED700B4556F /* Configuration */,
|
||||
504788EE2E681EC300B4556F /* Secrets */,
|
||||
504788ED2E681EB200B4556F /* Styles */,
|
||||
504788F02E681F0100B4556F /* Views */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@ -445,6 +486,7 @@
|
||||
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 */,
|
||||
@ -452,6 +494,7 @@
|
||||
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 */,
|
||||
@ -469,6 +512,7 @@
|
||||
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 */,
|
||||
|
@ -45,9 +45,9 @@ struct Secretive: App {
|
||||
ContentView(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
|
||||
.environment(EnvironmentValues._secretStoreList)
|
||||
.onAppear {
|
||||
if !hasRunSetup {
|
||||
// if !hasRunSetup {
|
||||
showingSetup = true
|
||||
}
|
||||
// }
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
||||
guard hasRunSetup else { return }
|
||||
|
@ -0,0 +1,49 @@
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
179
Sources/Secretive/Views/Configuration/Instructions.swift
Normal file
179
Sources/Secretive/Views/Configuration/Instructions.swift
Normal file
@ -0,0 +1,179 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
115
Sources/Secretive/Views/Configuration/IntegrationsView.swift
Normal file
115
Sources/Secretive/Views/Configuration/IntegrationsView.swift
Normal file
@ -0,0 +1,115 @@
|
||||
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)
|
||||
}
|
@ -67,7 +67,7 @@ struct SetupView: View {
|
||||
buttonWidth = width
|
||||
}
|
||||
.background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
|
||||
.frame(minWidth: 700, maxWidth: .infinity)
|
||||
.frame(minWidth: 600, maxWidth: .infinity)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(.setupDoneButton) {
|
||||
@ -154,17 +154,19 @@ struct StepView<Content: View>: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 20) {
|
||||
HStack(spacing: 0) {
|
||||
icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 24)
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Spacer()
|
||||
.frame(width: 20)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.bold()
|
||||
Text(description)
|
||||
}
|
||||
Spacer()
|
||||
Spacer(minLength: 20)
|
||||
actions
|
||||
}
|
||||
.padding(20)
|
@ -0,0 +1,110 @@
|
||||
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))
|
||||
}
|
||||
|
||||
}
|
@ -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,222 +0,0 @@
|
||||
import SwiftUI
|
||||
import SecretKit
|
||||
import SecureEnclaveSecretKit
|
||||
import SmartCardSecretKit
|
||||
import Brief
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@Binding var showingCreation: Bool
|
||||
@Binding var runningSetup: Bool
|
||||
@Binding var hasRunSetup: Bool
|
||||
@State var showingAgentInfo = false
|
||||
@State var activeSecret: AnySecret?
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@Environment(\.secretStoreList) private var storeList
|
||||
@Environment(\.updater) private var updater: any UpdaterProtocol
|
||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||
|
||||
@State private var selectedUpdate: Release?
|
||||
@State private var showingAppPathNotice = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if storeList.anyAvailable {
|
||||
StoreListView(activeSecret: $activeSecret)
|
||||
} else {
|
||||
NoStoresView()
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 640, minHeight: 320)
|
||||
.toolbar {
|
||||
toolbarItem(updateNoticeView, id: "update")
|
||||
toolbarItem(runningOrRunSetupView, id: "setup")
|
||||
toolbarItem(appPathNoticeView, id: "appPath")
|
||||
toolbarItem(newItemView, id: "new")
|
||||
}
|
||||
.sheet(isPresented: $runningSetup) {
|
||||
SetupView(setupComplete: $hasRunSetup)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ContentView {
|
||||
|
||||
|
||||
@ToolbarContentBuilder
|
||||
func toolbarItem(_ view: some View, id: String) -> some ToolbarContent {
|
||||
if #available(macOS 26.0, *) {
|
||||
ToolbarItem(id: id) { view }
|
||||
.sharedBackgroundVisibility(.hidden)
|
||||
} else {
|
||||
ToolbarItem(id: id) { view }
|
||||
}
|
||||
}
|
||||
|
||||
var needsSetup: Bool {
|
||||
runningSetup || !hasRunSetup
|
||||
}
|
||||
|
||||
/// Item either showing a "everything's good, here's more info" or "something's wrong, re-run setup" message
|
||||
/// These two are mutually exclusive
|
||||
@ViewBuilder
|
||||
var runningOrRunSetupView: some View {
|
||||
if needsSetup {
|
||||
setupNoticeView
|
||||
} else {
|
||||
agentStatusToolbarView
|
||||
}
|
||||
}
|
||||
|
||||
var updateNoticeContent: (LocalizedStringResource, Color)? {
|
||||
guard let update = updater.update else { return nil }
|
||||
if update.critical {
|
||||
return (.updateCriticalNoticeTitle, .red)
|
||||
} else {
|
||||
if updater.testBuild {
|
||||
return (.updateTestNoticeTitle, .blue)
|
||||
} else {
|
||||
return (.updateNormalNoticeTitle, .orange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var updateNoticeView: some View {
|
||||
if let update = updater.update, let (text, color) = updateNoticeContent {
|
||||
Button(action: {
|
||||
selectedUpdate = update
|
||||
}, label: {
|
||||
Text(text)
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
})
|
||||
.buttonStyle(ToolbarButtonStyle(color: color))
|
||||
.sheet(item: $selectedUpdate) { update in
|
||||
UpdateDetailView(update: update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var newItemView: some View {
|
||||
if storeList.modifiableStore?.isAvailable ?? false {
|
||||
Button(.appMenuNewSecretButton, systemImage: "plus") {
|
||||
showingCreation = true
|
||||
}
|
||||
.menuButton()
|
||||
.sheet(isPresented: $showingCreation) {
|
||||
if let modifiable = storeList.modifiableStore {
|
||||
CreateSecretView(store: modifiable) { created in
|
||||
if let created {
|
||||
activeSecret = created
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var setupNoticeView: some View {
|
||||
Button(action: {
|
||||
runningSetup = true
|
||||
}, label: {
|
||||
if !hasRunSetup {
|
||||
Text(.agentSetupNoticeTitle)
|
||||
.font(.headline)
|
||||
}
|
||||
})
|
||||
.buttonStyle(ToolbarButtonStyle(color: .orange))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var agentStatusToolbarView: 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
.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) {
|
||||
AgentStatusView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var appPathNoticeView: some View {
|
||||
if !ApplicationDirectoryController().isInApplicationsDirectory {
|
||||
Button(action: {
|
||||
showingAppPathNotice = true
|
||||
}, label: {
|
||||
Group {
|
||||
Text(.appNotInApplicationsNoticeTitle)
|
||||
}
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
})
|
||||
.buttonStyle(ToolbarButtonStyle(color: .orange))
|
||||
.popover(isPresented: $showingAppPathNotice, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
|
||||
VStack {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 64)
|
||||
Text(.appNotInApplicationsNoticeDetailDescription)
|
||||
.frame(maxWidth: 300)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var attachmentAnchor: PopoverAttachmentAnchor {
|
||||
.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
|
||||
|
@ -1,71 +0,0 @@
|
||||
import SwiftUI
|
||||
import SecretKit
|
||||
|
||||
struct EmptyStoreView: View {
|
||||
|
||||
@State var store: AnySecretStore?
|
||||
|
||||
var body: some View {
|
||||
if store is AnySecretStoreModifiable {
|
||||
EmptyStoreModifiableView()
|
||||
} else {
|
||||
EmptyStoreImmutableView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EmptyStoreImmutableView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(.emptyStoreNonmodifiableTitle).bold()
|
||||
Text(.emptyStoreNonmodifiableDescription)
|
||||
Text(.emptyStoreNonmodifiableSupportedKeyTypes)
|
||||
}.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct EmptyStoreModifiableView: View {
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { windowGeometry in
|
||||
VStack {
|
||||
GeometryReader { g in
|
||||
Path { path in
|
||||
path.move(to: CGPoint(x: g.size.width / 2, y: g.size.height))
|
||||
path.addCurve(to:
|
||||
CGPoint(x: g.size.width * (3/4), y: g.size.height * (1/2)), control1:
|
||||
CGPoint(x: g.size.width / 2, y: g.size.height * (1/2)), control2:
|
||||
CGPoint(x: g.size.width * (3/4), y: g.size.height * (1/2)))
|
||||
path.addCurve(to:
|
||||
CGPoint(x: g.size.width - 13, y: 0), control1:
|
||||
CGPoint(x: g.size.width - 13 , y: g.size.height * (1/2)), control2:
|
||||
CGPoint(x: g.size.width - 13, y: 0))
|
||||
}.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round))
|
||||
Path { path in
|
||||
path.move(to: CGPoint(x: g.size.width - 23, y: 0))
|
||||
path.addLine(to: CGPoint(x: g.size.width - 13, y: -10))
|
||||
path.addLine(to: CGPoint(x: g.size.width - 3, y: 0))
|
||||
}.fill()
|
||||
}.frame(height: (windowGeometry.size.height/2) - 20).padding()
|
||||
Text(.emptyStoreModifiableClickHereTitle).bold()
|
||||
Text(.emptyStoreModifiableClickHereDescription)
|
||||
Spacer()
|
||||
}.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct EmptyStoreModifiableView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
EmptyStoreImmutableView()
|
||||
EmptyStoreModifiableView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@ -1,417 +0,0 @@
|
||||
import SwiftUI
|
||||
import SecretKit
|
||||
|
||||
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 {
|
||||
|
||||
@Environment(\.secretStoreList) private var secretStoreList
|
||||
@State var creating = false
|
||||
|
||||
@State var selectedSecret: AnySecret?
|
||||
@Binding private var selectedInstruction: ConfigurationFileInstructions?
|
||||
private let instructions = Instructions()
|
||||
|
||||
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
|
||||
_selectedInstruction = selectedInstruction
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if let selectedInstruction {
|
||||
switch selectedInstruction.id {
|
||||
case .gettingStarted:
|
||||
Form {
|
||||
Section(.integrationsGettingStartedTitle) {
|
||||
Text(.integrationsGettingStartedTitleDescription)
|
||||
}
|
||||
Section {
|
||||
Group {
|
||||
Text(.integrationsGettingStartedSuggestionSsh)
|
||||
.onTapGesture {
|
||||
self.selectedInstruction = instructions.ssh
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(.integrationsGettingStartedSuggestionShell)
|
||||
Text(.integrationsGettingStartedSuggestionShellDefault(shellName: 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)
|
||||
case .tool:
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
private 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
IntegrationsView()
|
||||
.frame(height: 500)
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct NoStoresView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(.noSecureStorageTitle)
|
||||
.bold()
|
||||
Text(.noSecureStorageDescription)
|
||||
Link(.noSecureStorageYubicoLink, destination: URL(string: "https://www.yubico.com/products/compare-yubikey-5-series/")!)
|
||||
}.padding()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
struct NoStoresView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NoStoresView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
Loading…
Reference in New Issue
Block a user