mirror of
https://github.com/acamarata/curtain.git
synced 2026-07-01 11:14: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.
79 lines
4 KiB
Swift
79 lines
4 KiB
Swift
import SwiftUI
|
|
|
|
/// Purpose: Idle & End tab — idle-timeout block and on-disconnect block.
|
|
/// Extracted from PreferencesView to keep every tab file under 500 lines.
|
|
/// Inputs: @AppStorage bindings for idle and end prefs.
|
|
/// Outputs: writes to UserDefaults; no side-effectful closures needed.
|
|
/// Constraints: @MainActor (SwiftUI). Idle source labels updated to be self-explanatory
|
|
/// about what "session input" vs "HID" means for the remote operator.
|
|
/// SPORT: MASTER-PREFS
|
|
struct PrefIdleEndTab: View {
|
|
@AppStorage(Settings.Key.idleEnabled) private var idleEnabled = true
|
|
@AppStorage(Settings.Key.idleMinutes) private var idleMinutes = 30
|
|
// FIX-5: default literal aligned to registerDefaults (was "hidIdle", now "sessionInput")
|
|
@AppStorage(Settings.Key.idleSource) private var idleSource = "sessionInput"
|
|
@AppStorage(Settings.Key.onIdleDisconnect) private var idleDisconnect = true
|
|
@AppStorage(Settings.Key.onIdleLock) private var idleLock = true
|
|
@AppStorage(Settings.Key.onIdleScreenOff) private var idleScreenOff = true
|
|
@AppStorage(Settings.Key.onIdleDeactivate) private var idleDeactivate = true
|
|
@AppStorage(Settings.Key.onEndLock) private var endLock = true
|
|
@AppStorage(Settings.Key.onEndScreenOff) private var endScreenOff = true
|
|
@AppStorage(Settings.Key.onEndDeactivate) private var endDeactivate = true
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section {
|
|
Toggle("Act after the session is idle", isOn: $idleEnabled)
|
|
if idleEnabled {
|
|
Stepper("After \(idleMinutes) minutes of inactivity:", value: $idleMinutes, in: 1...240)
|
|
Picker("Inactivity is measured by", selection: $idleSource) {
|
|
// Tag values are unchanged; only the human-readable labels are updated
|
|
// to make it obvious that "session input" tracks the remote operator.
|
|
Text("Remote session activity (recommended)").tag("sessionInput")
|
|
Text("This Mac's physical input only").tag("hidIdle")
|
|
}
|
|
Toggle("Disconnect the remote session", isOn: $idleDisconnect)
|
|
Toggle("Lock the Mac", isOn: $idleLock)
|
|
Toggle("Turn off the displays", isOn: $idleScreenOff)
|
|
Toggle("Deactivate the curtain", isOn: $idleDeactivate)
|
|
}
|
|
} header: {
|
|
Text("On idle")
|
|
} footer: {
|
|
if idleEnabled {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
if idleMinutes <= 2 && idleDisconnect {
|
|
warn("A very short idle timeout with disconnect-on-idle can cut a session during a brief pause.")
|
|
}
|
|
if !idleDeactivate && idleScreenOff {
|
|
warn("Screens go dark on idle but the curtain stays up. The desk shows nothing until you deactivate.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Section {
|
|
Toggle("Lock the Mac", isOn: $endLock)
|
|
Toggle("Turn off the displays", isOn: $endScreenOff)
|
|
Toggle("Deactivate the curtain", isOn: $endDeactivate)
|
|
} header: {
|
|
Text("When the remote session disconnects")
|
|
} footer: {
|
|
if !endDeactivate && !endLock {
|
|
warn("On disconnect the Mac is neither locked nor uncovered. It is left covered but unlocked (\"dead but unlocked\").")
|
|
}
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
}
|
|
|
|
/// Small inline warning row: orange triangle + caption. Used in section footers
|
|
/// to flag dangerous setting combinations without alarming the layout.
|
|
private func warn(_ text: String) -> some View {
|
|
HStack(alignment: .top, spacing: 6) {
|
|
Image(systemName: "exclamationmark.triangle")
|
|
Text(text)
|
|
}
|
|
.font(.caption)
|
|
.foregroundStyle(.orange)
|
|
}
|
|
}
|