mirror of
https://github.com/acamarata/curtain.git
synced 2026-06-30 18:54:25 +00:00
Detection: netstat lives at /usr/sbin/netstat, not /usr/bin — the hardcoded wrong path silently killed the ESTABLISHED-TCP activator (root cause of the failed live test). Fixed and live-verified. Added peered-UDP activator (5900-5902) for High-Performance sessions, per-signal transition logging, unconditional error logging for dead probe helpers, and probe v2 with full CGSession dictionary diffing. 7 new parser tests (32 total). Fixes from a full audit + adversarial review: idle source setting honored (default now Remote session activity), cover scope reduced to a coherent two-mode model with legacy migration (per-display toggle was inverted in onlyMarked and dead in all), curtain test no longer schedules a teardown over a live session, specific-display password box placement gets a real picker, refuse-to-arm enforced, activation notification posts a real banner, menu password gate bypassed when the event tap is dead, shared single-decoder aerial player with stale-task guard and async playability check, password buffer zeroed on successful unlock and Esc, XPC interruption/invalidation handlers, modern Accessibility settings URL, launchPath modernized, codesign failures now abort release.sh, monotonic CFBundleVersion, install.sh temp cleanup, dead armDisarmHotkey setting removed. Refactor: Curtain.swift and PreferencesWindow.swift split into focused files (largest now 479 lines). Wiki, README, and contributing docs updated to match. Build clean at 0 warnings, 32/32 tests pass.
85 lines
4 KiB
Swift
85 lines
4 KiB
Swift
import SwiftUI
|
|
|
|
/// Purpose: Security tab — unlock action, password-box timeout, require-password
|
|
/// toggle, Accessibility behavior, and password change form.
|
|
/// Extracted from PreferencesView to keep every tab file under 500 lines.
|
|
/// Inputs: @AppStorage bindings for security prefs, plus a @Binding for the live
|
|
/// hasPassword state so the parent can refresh it after a reset.
|
|
/// Outputs: writes to UserDefaults; calls Settings.setPassword on "Set" button press.
|
|
/// Constraints: @MainActor (SwiftUI). The fallback password "curtain" always works so
|
|
/// the user can never be locked out — the UI calls this out in the footer.
|
|
/// SPORT: MASTER-PREFS
|
|
struct PrefSecurityTab: View {
|
|
// FIX-5: default literal aligned to registerDefaults ("keepSession" -> "disconnect")
|
|
@AppStorage(Settings.Key.onUnlockAction) private var onUnlockAction = "disconnect"
|
|
@AppStorage(Settings.Key.passwordBoxTimeoutSeconds) private var passwordBoxTimeout = 15
|
|
@AppStorage(Settings.Key.requirePasswordToDeactivateFromMenu) private var requirePasswordToDeactivate = false
|
|
@AppStorage(Settings.Key.accessibilityMissingBehavior) private var accessibilityMissing = "warn"
|
|
|
|
@State private var newPassword = ""
|
|
@Binding var hasPassword: Bool
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section {
|
|
Picker("On Curtain Unlock", selection: $onUnlockAction) {
|
|
Text("Keep the remote session active").tag("keepSession")
|
|
Text("Disconnect the remote session").tag("disconnect")
|
|
}
|
|
Stepper("Password box timeout: \(passwordBoxTimeout)s", value: $passwordBoxTimeout, in: 5...60)
|
|
Toggle("Require the password to deactivate from the menu", isOn: $requirePasswordToDeactivate)
|
|
} header: {
|
|
Text("Unlock")
|
|
} footer: {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
if onUnlockAction == "disconnect" {
|
|
Text("Disconnecting on unlock needs the disconnect helper enabled (see the Disconnect tab). Under an ad-hoc local build, enabling it installs a small privileged helper with one admin prompt.")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
if requirePasswordToDeactivate {
|
|
warn("The fallback password \"curtain\" always works, so you can never be locked out of your own Mac.")
|
|
}
|
|
}
|
|
}
|
|
Section {
|
|
Picker("If Accessibility is missing", selection: $accessibilityMissing) {
|
|
Text("Warn and arm anyway").tag("warn")
|
|
Text("Refuse to arm").tag("refuseToArm")
|
|
}
|
|
} header: {
|
|
Text("Accessibility")
|
|
} footer: {
|
|
if accessibilityMissing == "refuseToArm" {
|
|
warn("Curtain will not arm without Accessibility. Grant it in System Settings, or the curtain never engages.")
|
|
}
|
|
}
|
|
Section {
|
|
HStack {
|
|
SecureField("New unlock password", text: $newPassword)
|
|
Button("Set") {
|
|
if !newPassword.isEmpty {
|
|
Settings.setPassword(newPassword)
|
|
newPassword = ""
|
|
hasPassword = Settings.hasPassword
|
|
}
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Password")
|
|
} footer: {
|
|
Text(hasPassword ? "A password is set." : "No password set (default: \"curtain\").")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
}
|
|
|
|
private func warn(_ text: String) -> some View {
|
|
HStack(alignment: .top, spacing: 6) {
|
|
Image(systemName: "exclamationmark.triangle")
|
|
Text(text)
|
|
}
|
|
.font(.caption)
|
|
.foregroundStyle(.orange)
|
|
}
|
|
}
|