mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-09-20 03:10:57 +00:00
418 lines
16 KiB
Swift
418 lines
16 KiB
Swift
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)
|
|
}
|