145 lines
7.2 KiB
Swift
145 lines
7.2 KiB
Swift
import SwiftUI
|
|
import ServiceManagement
|
|
|
|
struct SettingsView: View {
|
|
@EnvironmentObject private var automation: BackgroundSyncManager
|
|
@AppStorage("defaultSSHUsername") private var defaultSSHUsername = NSUserName()
|
|
@AppStorage("defaultSSHKeyPath") private var defaultSSHKeyPath = ""
|
|
@AppStorage("defaultSSHPort") private var defaultSSHPort = 22
|
|
@AppStorage("defaultRemoteHost") private var defaultRemoteHost = ""
|
|
@AppStorage("backgroundSchedulesEnabled") private var backgroundSchedulesEnabled = true
|
|
@AppStorage("fileWatchingEnabled") private var fileWatchingEnabled = true
|
|
@AppStorage("defaultWatchDebounceSeconds") private var defaultWatchDebounceSeconds = 3
|
|
@AppStorage("historyRetentionDays") private var historyRetentionDays = 90
|
|
@State private var launchAtLogin = false
|
|
@State private var launchError: String?
|
|
|
|
var body: some View {
|
|
TabView {
|
|
Form {
|
|
Section("Default SSH Connection") {
|
|
TextField("Username", text: $defaultSSHUsername, prompt: Text(NSUserName()))
|
|
TextField("Default host", text: $defaultRemoteHost, prompt: Text("server.local"))
|
|
Stepper("SSH port: \(defaultSSHPort)", value: $defaultSSHPort, in: 1...65535)
|
|
|
|
HStack {
|
|
TextField("Private SSH key", text: $defaultSSHKeyPath, prompt: Text("~/.ssh/id_ed25519"))
|
|
Button("Choose…") {
|
|
if let selected = FilePicker.chooseFile() { defaultSSHKeyPath = selected }
|
|
}
|
|
}
|
|
|
|
Label("Authentication uses an SSH private key or your macOS SSH agent. Passwords are intentionally not stored or passed on the command line.", systemImage: "lock.shield")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Section("New Jobs") {
|
|
Text("New SSH jobs use these values as defaults and can override them independently.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
.tabItem { Label("Connections", systemImage: "network") }
|
|
|
|
Form {
|
|
Section("Background Scheduling") {
|
|
Toggle("Run scheduled jobs", isOn: $backgroundSchedulesEnabled)
|
|
Toggle("Launch Rsync Manager at login", isOn: Binding(
|
|
get: { launchAtLogin },
|
|
set: { setLaunchAtLogin($0) }
|
|
))
|
|
HStack {
|
|
Label("Configured jobs", systemImage: "calendar.badge.clock")
|
|
Spacer()
|
|
Text("\(automation.scheduledJobCount)")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Text("Scheduled jobs run without opening a modal and are written to History. Rsync Manager continues processing them while its window is hidden, minimised or closed, provided the application itself remains running.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Section("History & Privacy") {
|
|
Stepper("Keep run history for \(historyRetentionDays) days", value: $historyRetentionDays, in: 7...3650, step: 7)
|
|
Text("Run output is sanitised, ANSI control sequences are removed, and individual stdout/stderr logs are capped at approximately 2 MB before storage.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Section("File Watching") {
|
|
Toggle("Watch enabled jobs for changes", isOn: $fileWatchingEnabled)
|
|
Stepper("Default debounce: \(defaultWatchDebounceSeconds) seconds", value: $defaultWatchDebounceSeconds, in: 1...60)
|
|
HStack {
|
|
Label("Watched jobs", systemImage: "eye.fill")
|
|
Spacer()
|
|
Text("\(automation.watchedJobCount)")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Text("Rsync Manager uses macOS FSEvents, so watched trees do not need to be repeatedly scanned. A short debounce combines rapid saves into a single sync.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
.tabItem { Label("Automation", systemImage: "calendar.badge.clock") }
|
|
|
|
Form {
|
|
Section("Rsync") {
|
|
LabeledContent("Detected executable", value: RsyncLocator.locate())
|
|
LabeledContent("Executable validation", value: RsyncLocator.isTrustedExecutableCandidate(RsyncLocator.locate()) ? "Passed" : "Failed")
|
|
Text("Only executable regular-file rsync candidates are accepted. Symlinks are resolved before validation.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Section("Security") {
|
|
LabeledContent("SSH mode", value: "Batch / key or agent")
|
|
LabeledContent("Host-key verification", value: "System default")
|
|
Text("SSH connections use BatchMode, connection timeouts and keepalive limits. Rsync Manager never disables SSH host-key verification.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Section("Application") {
|
|
LabeledContent("Bundle identifier", value: Bundle.main.bundleIdentifier ?? "net.absdev.RsyncManager")
|
|
LabeledContent("Version", value: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0")
|
|
LabeledContent("Background service", value: automation.activity.title)
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
.tabItem { Label("General", systemImage: "gearshape") }
|
|
}
|
|
.frame(width: 650, height: 455)
|
|
.onAppear {
|
|
KeychainStore.delete("defaultSSHPassword")
|
|
launchAtLogin = SMAppService.mainApp.status == .enabled
|
|
}
|
|
.onChange(of: backgroundSchedulesEnabled) { _, _ in automation.refreshConfiguration() }
|
|
.onChange(of: fileWatchingEnabled) { _, _ in automation.refreshConfiguration() }
|
|
.alert("Launch at Login", isPresented: Binding(
|
|
get: { launchError != nil },
|
|
set: { if !$0 { launchError = nil } }
|
|
)) {
|
|
Button("OK", role: .cancel) { launchError = nil }
|
|
} message: {
|
|
Text(launchError ?? "Unable to change the login item setting.")
|
|
}
|
|
}
|
|
|
|
private func setLaunchAtLogin(_ enabled: Bool) {
|
|
do {
|
|
if enabled {
|
|
try SMAppService.mainApp.register()
|
|
} else {
|
|
try SMAppService.mainApp.unregister()
|
|
}
|
|
launchAtLogin = SMAppService.mainApp.status == .enabled
|
|
} catch {
|
|
launchAtLogin = SMAppService.mainApp.status == .enabled
|
|
launchError = error.localizedDescription
|
|
}
|
|
}
|
|
}
|